Skip to content

Drop application-state <meta> from front matter; one front matter reader for titles (#82) - #86

Merged
morisil merged 25 commits into
mainfrom
drop-meta-application-state
Sep 30, 2026
Merged

morisil merged 25 commits into
mainfrom
drop-meta-application-state

Conversation

@morisil

@morisil morisil commented Sep 30, 2026

Copy link
Copy Markdown
Member

Closes #82.

What changes

simplifyHtml: application-state <meta> values are dropped from the front matter.
isApplicationStateMeta (ApplicationStateMeta.kt) judges a <meta content> by its value, not its name:
serialised state (strict JSON objects and nested arrays, base64-like blobs, long unbroken tokens) is dropped,
while prose, flat word/number arrays and lists of URLs are kept.
On the LinkedIn "People" page this takes the front matter from 782 KB (95% of the dump) back to real page metadata.

Title and head metadata.

  • The first non-blank <title> wins instead of every <title> in <head> being concatenated.
  • <meta> names are read ASCII case-insensitively, first <meta> wins as in HTML, blank values are skipped (HeadMetadata.kt, shared by simplifyHtml and wrapInHtmlDocument).

wrapInHtmlDocument and ensureFrontmatterTitle read front matter through one reader (FrontMatterEntryReader),
so they agree on which title entry is read and what a usable one is.
ensureFrontmatterTitle never leaves duplicate title keys (js-yaml rejects them and loses the whole front matter),
keeps nested and verbatim titles as content,
and keeps its output balanced when the stream ends inside a frontmatter or h1.

Supporting pieces.
AsciiCase and HTML-whitespace helpers in markanywhere-html-spec;
yamlKeyLineKeyOrNull in markanywhere-yaml, so a verbatim YAML line can still be matched to the key it defines.

Known gaps — tracked in #85

FrontMatterEntryReader infers whether an entry's value is known from the shape of the events.
The shape is ambiguous, so review rounds kept finding new multi-line YAML shapes.
Rather than keep patching, this PR takes the conservative option:
an entry continued on indented lines, or holding indented verbatim lines under a bare key:, is read as unknown
(no <title> / <meta>, kept as content by ensureFrontmatterTitle) instead of as a truncated or indented value.

The proper fix — YamlParser marking such entries itself — and the remaining edge cases
(merged text events hiding a verbatim title: line, key-shaped verbatim lines blocking detection, truncated or crossed upstream streams)
are in #85.

Testing

  • ./gradlew build passes locally (dev targets).
  • New regression tests in SimplifyHtmlTest, WrapInHtmlDocumentTest, EnsureFrontmatterTitleTest, AsciiCaseTest, HtmlWhitespaceTest, YamlKeyLineTest.
  • The W3C validator dump goldens change by one line each.

🤖 Generated with Claude Code

morisil and others added 25 commits September 28, 2026 14:07
…nk title wins (#82)

Single-page apps ship serialised state in <meta content> under
framework-private names (LinkedIn's __init, Ember's percent-encoded
config/environment, ...), swamping the front matter. simplifyHtml now
judges the value: a leading JSON object/array (raw or percent-encoded),
a value over 4096 chars, or an HTML-blank value is dropped. Bracketed
prefixes like "[Solved] ..." survive.

When <head> holds several <title>s, the first non-blank one wins, and
it takes precedence over <meta name="title"> in either order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tes at </head> (#82)

isApplicationStateMeta moves to ApplicationStateMeta.kt and now parses
the value as a JSON object/array (raw or percent-decoded) instead of
peeking at its first and last char, so bracketed human text like
"[2024] Annual report [PDF]" survives. A blank value is dropped on its
own, not as "application state".

Title candidates are resolved when <head> closes: the first non-blank
<title> wins over <meta name="title"> (matched case-insensitively),
and the entry keeps the position of the first candidate. Documented
which values don't survive a wrapInHtmlDocument round-trip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#82)

isApplicationStateMeta now parses with kotlinx Json.parseToJsonElement
instead of a hand-rolled JSON validator. A JSON object is still dropped,
but an array survives when it is a flat list of strings or numbers
(keywords, article:tag, citation_volume); empty, nested or
flag/null-holding arrays, and percent-encoded arrays, are dropped.

Meta names are ASCII case-insensitive (HTML §4.2.5): of several names
differing only in letter case, the first spelling and value win, and
the title candidate logic folds into the same first-wins map. Documented
the extra wrapInHtmlDocument round-trip caveat and the kotlinx Json
leniency gotcha in CLAUDE.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e meta values (#82)

Move HTML's ASCII lowercase into markanywhere-html-spec (asciiLowercase)
and use it in simplifyHtml, wrapInHtmlDocument, ensureFrontmatterTitle
and AutolinkCollector instead of private copies. wrapInHtmlDocument and
ensureFrontmatterTitle now read front matter keys the way HTML reads
<meta> names, so a Title key is the title.

isApplicationStateMeta judges a percent-encoded value by what it
decodes to, keeps flat arrays (an empty one included), and drops an
over-long value only when it does not read as text, so a long prose
abstract survives. JSON parse failures of any kind are treated as
"not JSON". A blank (NBSP-only included) <html lang> is skipped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#82)

- Extract HeadMetadata: first of case-variant duplicate keys wins in both
  simplifyHtml and wrapInHtmlDocument (wrapInHtmlDocument used last-wins).
- ensureFrontmatterTitle judges only the first title entry in any case, so
  a usable title can no longer hide a blank variant that blanks <title>.
- Strip and collapse <title> whitespace as document.title does
  (stripAndCollapseHtmlWhitespace in markanywhere-html-spec).
- Spell a lang meta as `lang`, yielding to <html lang>.
- Keep long comma/semicolon-separated keyword lists as text; decode
  percent escapes with ASCII hex digits only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ment; tidy head metadata (#82)

- ensureFrontmatterTitle judges the first scalar title entry, the one
  wrapInHtmlDocument reads, instead of the first title entry of any type;
  a replaced entry keeps its key spelling
- strip and collapse a title meta like a <title> element; trim lang
- detect JSON state serialised into a JSON string
- count code points when judging whether a long value reads as text
- share HEAD_KEYS / SCALAR_ENTRY_TYPES via HeadMetadata.kt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… keys (#82)

- HeadMetadata.add ignores blank values, so a blank key never shadows a
  later case variant in simplifyHtml or wrapInHtmlDocument
- ensureFrontmatterTitle judges the first non-blank scalar title entry,
  respells a case-variant key to `title`, and collapses the derived
  title's whitespace as document.title does
- treat double-encoded JSON state inside arrays as application state;
  count combining marks as letters when judging prose
- drop unused kotlinx-serialization-json dependency declaration

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- judge a percent-encoded meta value, including its length, by what it
  decodes to, and a flat JSON array by its words, so long keyword lists
  survive the over-long backstop
- replace the empty title slot spelled `title` first, inject a title next
  to an unreadable variant in another spelling, and treat a frontmatter
  that does not open the stream (or a tagged one) as content, as
  wrapInHtmlDocument does
- share isScalarEntryType / isMetadataValue between wrapInHtmlDocument
  and ensureFrontmatterTitle; drop the redundant lang blank check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cycle

Each step runs as a separate headless `claude -p` process, so every review
starts from a fresh context. The loop stops when no critical/major
correctness finding remains, when the fixer commits nothing, when a round
leaves the build red, or at the round limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oop (#82)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the sentinel

- stop when a round commits kotlin-js-store/yarn.lock, which the JS test
  build narrows, and revert the lock after every fix round
- check for uncommitted changes before honouring the sentinel, so a fixer
  that edited files cannot end the loop with a dirty tree

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tter titles readable (#82)

- simplifyHtml keeps the first of duplicate <meta> names (HTML);
  wrapInHtmlDocument keeps the later front matter entry (Psych/PyYAML),
  the lowercase spelling beating a case variant
- ensureFrontmatterTitle drops blank/null entries spelled `title`, respells
  the usable variant wrapInHtmlDocument picks, and accepts a frontmatter
  preceded by blank text, emitting it first
- add String.stripHtmlWhitespace() to markanywhere-html-spec and use it in
  place of private trims
- skip JSON array word joining for meta values within the length limit

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a shadowing one (#82)

- wrapInHtmlDocument reads a frontmatter preceded by blank text, moving
  that text to the start of body, as ensureFrontmatterTitle does
- ensureFrontmatterTitle moves a usable `title` entry after a later
  collection-valued `title` entry, which a later-wins reader would pick
- simplifyHtml dedupes meta names on the normalized name (first wins)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ne (#82)

- normalizeTitle trims non-breaking space at a title's edges (inside it
  stays content), shared by simplifyHtml and ensureFrontmatterTitle
- ensureFrontmatterTitle puts a derived title right after a later
  collection-valued `title` entry, which a later-wins reader would pick,
  and picks the usable entry via frontMatterKeySupersedes, the rule
  HeadMetadata.addFromFrontMatter uses
- use getOrPutIfMissing for first-wins HTML metadata
- declare kotlinx-serialization-json directly for ApplicationStateMeta

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ensureFrontmatterTitle leaves exactly one title entry, spelled
  `title`, whenever a title comes out, dropping every other title
  variant: js-yaml (gray-matter) rejects duplicate keys outright, so
  reordering duplicates for later-wins readers was not enough
- ensureFrontmatterTitle and wrapInHtmlDocument treat only HTML
  whitespace ahead of the frontmatter as insignificant (an NBSP is
  content)
- isMetadataValue rejects zero-width / format chars as well as
  whitespace; simplifyHtml judges <title> text with it after
  normalization
- isApplicationStateMeta: a flat JSON array of scalars is metadata
  whatever its bare words parse as; the length backstop judges a value
  as written (not percent-decoded) and drops an over-long blob unparsed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- wrapInHtmlDocument and ensureFrontmatterTitle read the front matter
  through one FrontMatterEntryReader instead of private copies
- ensureFrontmatterTitle keeps a nested title entry (localized titles)
  rather than deriving one from the h1, dropping only empty variants
- normalizeTitle trims invisible format chars (zero-width space, BOM)
  at a title's edges; simplifyHtml judges meta blanks by isMetadataValue
- isApplicationStateMeta measures a flat JSON array by its elements
  joined, and drops a long array opening with a nested element unparsed
  unless it reads as text
- document why asciiLowercase differs from lowercase (Kelvin sign)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…82)

- isApplicationStateMeta percent-decodes repeatedly (bounded) and inside
  JSON strings, and spots a nested element anywhere in an over-long array
- ensureFrontmatterTitle drops empty title entries when no title is
  derived, and derives a title past NBSP text before the h1 (which still
  keeps a following frontmatter from opening the stream)
- normalizeTitle collapses line-breaking whitespace; HeadMetadata judges
  front matter entries by FrontMatterEntry.isHeadMetadata
- condense the front matter title entry in CLAUDE.md

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ensureFrontmatterTitle keeps a usable title variant beside a nested
  title entry as spelled, and leaves a frontmatter whose root is a
  sequence unchanged (FrontMatterEntryReader.isSequence)
- isApplicationStateMeta shares one layer budget between percent and
  JSON-string decoding, trims invisible chars (BOM, NBSP) at the edges,
  and judges an over-long value as written before decoding it
- wrapInHtmlDocument strips HTML whitespace around the lang entry
- extract trimInvisible; condense the simplifyHtml KDoc

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ensureFrontmatterTitle drops a frontmatter left empty by dropping its
  empty title entries (it would render as `---` twice, two thematic
  breaks on re-parse), and judges the title entries of a frontmatter
  the stream ends inside, an open entry included
- isMetadataValue treats control chars (next line, C0) as invisible
- clarify the over-long nested-array comment in isApplicationStateMeta

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ensureFrontmatterTitle keeps, beside a nested title entry, only the
  title variant wrapInHtmlDocument reads (via HeadMetadata, now tracking
  the source entry), and drops a frontmatter arriving empty even when
  no title can be derived
- share the frontmatter-opens-the-stream rule (opensFrontmatter)
  between wrapInHtmlDocument and ensureFrontmatterTitle
- isApplicationStateMeta judges a flat array past MAX_PARSED_LENGTH as
  written instead of parsing it; restructure into named helpers
- normalizeLang trims invisible chars (BOM, zero-width space) around
  the lang in simplifyHtml and wrapInHtmlDocument

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ensureFrontmatterTitle closes a frontmatter or h1 the stream ends
  inside, so its output stays balanced, and treats text showing nothing
  (BOM, zero-width space) around the h1 as blank, as a title is judged
- pick the title entry via FrontMatterEntry.readOver, shared with
  HeadMetadata, instead of tracking the source entry in MetadataEntry
- wrapInHtmlDocument normalizes the title as simplifyHtml reads a
  <title>
- isApplicationStateMeta counts `/` as a word break, so a long list of
  URLs is kept while base64 stays dropped
- drop the now unused stripHtmlWhitespace from markanywhere-html-spec
- restructure the ensureFrontmatterTitle and wrapInHtmlDocument KDoc

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ensureFrontmatterTitle no longer drops a leading title entry when that
  would leave a verbatim line first (front matter detection needs a key
  on line 2): a kept title variant moves into the first slot instead, or
  the first slot stays
- isMetadataValue treats blank-rendering glyphs (Hangul fillers, blank
  Braille pattern) as invisible
- isApplicationStateMeta never parses a value past MAX_PARSED_LENGTH,
  dropping one that opens like encoded state, and counts only letters of
  scripts written without spaces as word breaks
- dropHtmlStructuralWhitespace reuses stripAndCollapseHtmlWhitespace
- wrapInHtmlDocument checks the frontmatter mark alone (isFrontmatterMark)
- html-spec tests use sameAs for string comparisons

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- FrontMatterEntryReader spans each top-level entry over the indented
  verbatim lines continuing it, and reads a verbatim line defining a key
  as an entry of unknown value (yamlKeyLineKeyOrNull, new in
  markanywhere-yaml), never head metadata
- ensureFrontmatterTitle derives no title beside a verbatim title line
  (a multi-line quoted scalar), which made a duplicate key, and drops a
  title entry together with its continuation lines, which otherwise
  continued the entry before it; the verbatim-first guard is decided
  without building the body twice
- normalizeTitle keeps bidi controls at the title's edges
- isApplicationStateMeta judges only strict JSON as state, so bracketed
  prose such as [[Wiki]] is kept, and classifies supplementary-plane
  chars by plane, so a blob of emoji no longer reads as text
- HeadMetadata.acceptsFromHtml is the one first-wins and blank rule,
  shared with simplifyHtml's meta pre-check

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
)

An entry continued on indented lines, or holding indented verbatim lines
under a bare `key:`, has a value front matter readers read but the YAML
subset keeps verbatim. FrontMatterEntryReader marks it verbatim, so
wrapInHtmlDocument emits no truncated or indented <title>/<meta> and
ensureFrontmatterTitle keeps it as content. The proper fix, marking such
entries in YamlParser, is tracked in #85.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

The PR is a draft, so step 1 says to stop and not proceed with the review.

Code review of PR #86 (xemantic/markanywhere): skipped. The PR is open but still a draft, and the review instructions say not to review drafts. I posted no comments on GitHub and no review was run.

No Claude comments exist on the PR yet. When the PR is marked ready for review, running /code-review:code-review again will start a full review.

Posted by the workflow: the review agent finished without commenting.

@morisil
morisil marked this pull request as ready for review September 30, 2026 13:54
@morisil
morisil merged commit 2ab2ca1 into main Sep 30, 2026
2 checks passed
@morisil
morisil deleted the drop-meta-application-state branch September 30, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

simplifyHtml: front matter swamped by application-state <meta> tags; multiple <title>s concatenated

1 participant