Extract Markdown parse/produce as public pkg/markdown library - #343
Conversation
Introduce github.com/jeduden/mdsmith/pkg/markdown as the one public Markdown surface: Parse (front matter + body + AST), Splice (the edit-based producer), ParseContext/NewParser/StripFrontMatter/ CountLines, and the ProcessingInstruction node primitive. The canonical goldmark config and the PI block parser are moved here verbatim from internal/lint so a single config can back every parse path. internal/lint is repointed in a follow-up commit. https://claude.ai/code/session_01RBQSS4X7e6mfrkyXmDzGRX
internal/lint's PI node, PI block parser, canonical goldmark config, parser pool, and front-matter split are now thin aliases/forwards to pkg/markdown. NewFile and LinkReferences parse via markdown.ParseContext, so exactly one goldmark configuration backs every parse path. The ~150 lint consumers compile unchanged (ProcessingInstruction is a type alias). The moved primitives' unit tests now live in pkg/markdown; lint keeps the NewFile integration smoke and its YAML-decoder tests. Full go test ./... stays green. https://claude.ai/code/session_01RBQSS4X7e6mfrkyXmDzGRX
…plan 163) The MDS034 dual parser registered lint.PIBlockParserPrioritized, making a parse path depend on the linter core. It now uses markdown.PIBlockParserPrioritized from pkg/markdown; the rule logic (detect/fix/rule) still uses lint.File as before. No public parse path imports internal/lint. https://claude.ai/code/session_01RBQSS4X7e6mfrkyXmDzGRX
…lan 163) Characterization golden over every docs/**/*.md plus synthetic edge cases (setext H1, indented ATX, unterminated PI, frontmatter-only, malformed frontmatter, PI in a code fence). Generated from the pre-migration goldmark path; it is the AC4 byte-identical guard for the upcoming pkg/markdown migration of sync-docs. https://claude.ai/code/session_01RBQSS4X7e6mfrkyXmDzGRX
…ark config (plan 163) reconcileDocForHugo dropped its interim goldmark.New() parser (the PR #291 stopgap) for the canonical pkg/markdown parser and markdown.Splice. Directive markers are now ProcessingInstruction nodes instead of CommonMark type-3 HTML blocks; the span math is unchanged. The docs-corpus golden confirms byte-identical output before and after. Only one goldmark configuration now exists in the tree. https://claude.ai/code/session_01RBQSS4X7e6mfrkyXmDzGRX
Add docs/development/markdown-library.md documenting the pkg/markdown parse/produce API and its compatibility policy (task 6). Resolve the architecture doc/code conflict: go.md no longer claims internal/mdtext is the only goldmark home — pkg/markdown owns the parser config, mdtext walks the AST it produces. Register pkg/markdown in the cross-system boundaries table and versioning policy, and add it to the hub layering map and the inline-parsing anti-pattern. Regenerated catalogs (CLAUDE.md, AGENTS.md, copilot-instructions, dev index, PLAN.md); trimmed two CLAUDE.md prose lines to stay within its length limit after the new catalog rows. Refreshed the sync-docs golden for the grown docs corpus. Plan 163 marked complete: all tasks and acceptance criteria verified. https://claude.ai/code/session_01RBQSS4X7e6mfrkyXmDzGRX
There was a problem hiding this comment.
Pull request overview
This PR extracts mdsmith's Markdown parsing and producing logic from internal/lint into a new public Go package pkg/markdown. The new package owns the single canonical goldmark parser configuration (CommonMark + <?…?> processing-instruction block parser), the Parse/ParseContext entry points with frontmatter splitting, the Splice byte-exact producer, and the ProcessingInstruction AST node. internal/lint now re-exports these via type aliases and forwarders, internal/release/syncdocs.go migrates off its private goldmark.New() parser onto the shared one, and internal/rules/markdownflavor/parser.go switches to the public PI block-parser registration. A new golden corpus test pins reconcileDocForHugo output byte-for-byte across docs/** plus synthetic edge cases. Plan 163 is marked ✅.
Changes:
- New
pkg/markdownpackage withParse,ParseContext,NewParser,StripFrontMatter,CountLines,Splice/Edit, andProcessingInstructionplus comprehensive unit tests, package docs, and a parser pool. internal/lintandinternal/release/syncdocs.gorefactored to consumepkg/markdown; lint keeps thin forwarders/type aliases for backward compatibility; PI grammar/frontmatter unit tests relocated next to the canonical implementation.- New
docs/development/markdown-library.md(compatibility policy, API surface) plus catalog/architecture/cross-system doc updates; newsyncdocs_golden_test.gopins reconciliation output.
Reviewed changes
Copilot reviewed 32 out of 33 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/markdown/parser.go | Canonical goldmark parser + pooled ParseContext. |
| pkg/markdown/parse.go | Parse + Document (frontmatter split + AST). |
| pkg/markdown/parse_test.go | Unit tests for Parse, ParseContext, Splice. |
| pkg/markdown/parser_test.go | Parser smoke + concurrent race-free pool test. |
| pkg/markdown/produce.go | Edit + Splice byte-exact span surgery. |
| pkg/markdown/pi.go | ProcessingInstruction AST node + KindProcessingInstruction. |
| pkg/markdown/pi_parser.go | Block parser for <?…?> PIs, with extractPINameBytes. |
| pkg/markdown/pi_test.go | Relocated PI grammar tests + parser/extract coverage. |
| pkg/markdown/frontmatter.go | StripFrontMatter + CountLines. |
| pkg/markdown/frontmatter_test.go | Frontmatter behavior incl. block-scalar fence regression. |
| pkg/markdown/doc.go | Package documentation and compatibility pointer. |
| internal/lint/pi.go | Reduced to type-alias forwarder over pkg/markdown. |
| internal/lint/pi_parser.go | Reduced to forwarders for NewPIBlockParser/PIBlockParserPrioritized. |
| internal/lint/pi_test.go | Removed PI grammar tests now living in pkg/markdown. |
| internal/lint/frontmatter.go | Forwarders to pkg/markdown for frontmatter helpers. |
| internal/lint/frontmatter_test.go | Removed forwarded-helper tests. |
| internal/lint/file.go | NewParser/parse path delegates to markdown.ParseContext. |
| internal/lint/lint_coverage_test.go | Removed tests now in pkg/markdown; left explanatory comments. |
| internal/rules/markdownflavor/parser.go | Uses markdown.PIBlockParserPrioritized instead of lint.*. |
| internal/rules/markdownflavor/parser_test.go | Updated import/comment to reference pkg/markdown. |
| internal/release/syncdocs.go | Migrated to markdown.ParseContext/Splice; matches PI block nodes. |
| internal/release/syncdocs_golden_test.go | New golden corpus test pinning reconcileDocForHugo output. |
| docs/development/markdown-library.md | New page: API surface + compatibility policy. |
| docs/development/architecture/{index,go,cross-system}.md | Reflect pkg/markdown as the single parse surface. |
| docs/development/index.md, CLAUDE.md, AGENTS.md, .github/copilot-instructions.md | Catalog/link updates for the new doc; minor wording trims. |
| plan/163_public-markdown-library.md, PLAN.md | Mark plan 163 ✅ and record decisions. |
…ixtures The plan-163 AC4 byte-identical proof was a full-docs-corpus snapshot captured pre-migration; it passed unchanged at b150678 and has served its purpose. As a permanent artifact it was wrong: re-rendering all of docs/ churned ~22.8k lines of testdata on every docs edit (and broke CI test once docs changed). Replace it with 12 minimal Markdown fixtures under testdata/reconcile/<case>/{in,golden}, one per reconcileDocForHugo branch (ATX/setext/indented/no-FM/existing-title/no-liftable-H1 title lift; single/multi-line/in-fence/unterminated PI strip; malformed and FM-only). 141 fixture lines total vs 22,806. Inputs are checked in as files like rule fixtures and use no .md extension so mdsmith does not lint the deliberately malformed cases. Also document in docs/development/markdown-library.md how pkg/markdown differs from stock goldmark (PI node vs type-3 HTML, no renderer, byte-split front matter, CommonMark-only, byte-stability contract). https://claude.ai/code/session_01RBQSS4X7e6mfrkyXmDzGRX
Stray artifact from an interim golden approach; no test references it. https://claude.ai/code/session_01RBQSS4X7e6mfrkyXmDzGRX
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files
☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
internal/release/syncdocs_test.go already exercises reconcileDocForHugo
exhaustively via inline TestReconcileDocForHugo_{TitleLift,TitleNoOp,
StripMarkers,StripNoOp} table tests (the project's established style).
The golden corpus / per-case testdata I added was redundant with that
pre-existing coverage from the start — remove syncdocs_golden_test.go
and testdata/reconcile/ entirely. No release testdata, no duplication;
the migrated reconcileDocForHugo stays guarded by those inline tests
plus pkg/markdown's own unit tests.
Also: delete the dead lint.NewPIBlockParser forward (nothing calls it;
lint.PIBlockParserPrioritized is kept — internal/schema uses it). Add
direct-call unit tests for the PI parser's defensive Open/Continue
guards so pkg/markdown is 100% covered (codecov/patch). Fix the
compatibility-policy reference in markdown-library.md that pointed at
the removed corpus file (Copilot review).
https://claude.ai/code/session_01RBQSS4X7e6mfrkyXmDzGRX
Plan 185 extends 163. It promotes the five custom goldmark extensions, the Flavor identity, and the feature-support model into a public pkg/markdown/flavor sub-package; decouples Detect from internal/lint; and retires the two remaining hand-rolled goldmark configs (internal/schema, markdownflavor) so one parser config remains. Tasks and acceptance criteria are annotated with the SOLID principle each verifies and the dependency direction each crosses, per the solid-architecture plan-mode workflow. Deliverable is the design plan only; no source changed. https://claude.ai/code/session_01RBQSS4X7e6mfrkyXmDzGRX
Reframe the move task as "every custom parser" with an explicit inventory: an exhaustive scan (ast.NewNodeKind / Extend / ASTTransformer) finds only the PI parser (already public via 163) and the five markdownflavor/ext extensions, including the abbreviation ASTTransformer. Add a verifiable acceptance criterion that no custom parser, node kind, or transformer remains under internal/ or cmd/. https://claude.ai/code/session_01RBQSS4X7e6mfrkyXmDzGRX
Extend the check-performance gate to the public pkg/markdown library (extracted in #343), which had no p95 backstop of its own despite being a cross-system compatibility contract. Add tiered BenchmarkParse{Small,Large} (canonical parser incl. the <?...?> PI block) and a markdown-bench CI job mirroring check-bench. Pass 2 of the task-11 profiler loop on the post-#343 hot path: unicode.IsSpace was ~5.5% of single-core check CPU (per-rune in CountWords/CountSentences and MDS024 cheapBounds). Add mdtext.IsSpace, an inlinable ASCII fast path proven byte-for- byte equivalent to unicode.IsSpace by an exhaustive rune-domain sweep. Single-core BenchmarkCheckCorpusLarge ~962 -> ~910-957 us/file. The #343 extraction preserved the Pass-1 gains. https://claude.ai/code/session_017rv18FDob5NHAjSdSvfeha Co-authored-by: Claude <noreply@anthropic.com>
Completes plan #163 by extracting mdsmith's Markdown parsing and production capabilities into a public Go library at
pkg/markdown.Summary
This PR establishes
pkg/markdownas mdsmith's canonical, importable Markdown surface. It moves parsing logic, processing instruction handling, and frontmatter utilities frominternal/lintinto the public package, making them available to external consumers while maintaining a single goldmark parser configuration across the codebase.Key Changes
New public package
pkg/markdown: Exports the canonical goldmark parser and Markdown processing utilitiesparser.go:NewParser()returns the shared parser instance with CommonMark + processing instruction supportparse.go:Parse()andDocumenttype for parsing Markdown with frontmatter separationpi.go&pi_parser.go: Processing instruction (<?...?>) AST node and parserfrontmatter.go:StripFrontMatter()utility for YAML frontmatter handlingproduce.go:EditandSplice()for Markdown mutationdoc.go: Package documentation and compatibility policyRefactored
internal/lint: Now imports frompkg/markdowninstead of duplicating logicpi.go,pi_parser.go,frontmatter.goreduced to thin wrappers or removedfile.goandlint_coverage_test.goupdated to use public APIspi_test.gomoved topkg/markdown/pi_test.gowith expanded coverageUpdated internal consumers:
internal/release/syncdocs.goandinternal/rules/markdownflavor/parser.gonow import frompkg/markdownDocumentation: Added
docs/development/markdown-library.mddescribing the public API, compatibility policy, and usageArchitecture updates: Updated architecture docs to reflect
pkg/markdownas the single Markdown surface boundaryTest infrastructure: Added golden-file test for
syncdocsreconciliation and comprehensive unit tests for all public APIsPlan status: Marked plan Clean up completed and won't-fix plans #163 as complete (✅)
Implementation Details
sync.Onceto ensure a single instance across the applicationhttps://claude.ai/code/session_01RBQSS4X7e6mfrkyXmDzGRX