Documentation
¶
Overview ¶
Package mdembed implements the marker grammar, hash normalization, and SVG metadata embedding shared between cmd/md-embed and any other tool that needs to read or write in-place ipmt rendering markers in Markdown.
Marker shape (two lines per embed — required for CommonMark to render the image; if the HTML comment and image were on the same line, the comment would open an HTML block that swallows the image):
<!-- ipm-svg id=01 hash=ab12cd34 [path=./alt.svg] [pos=before] --> 
The marker is independent of *how* the source was declared. A `\`\`\`ipmt` fence and an `<!-- ipm-include src=... -->` line are two equivalent ways to declare a block; both produce the same marker shape.
Package mdembed — srcsvg.go ¶
RenderSourceSVG converts ipmt source text into a syntax-coloured SVG, for embedding in Markdown rendered by hosts that do not natively highlight the `ipmt` language (notably GitHub). The output is a self-contained SVG with inline fill colours, no external CSS, no external fonts — same conventions as the diagram-SVG output produced by the rest of this package.
Token classes mirror the four scopes declared in the VS Code extension's tmLanguage grammar (vscode-ipm/syntaxes/ipmt.tmLanguage.json):
comment.line.number-sign.ipmt -> srcTokComment string.quoted.double.ipmt -> srcTokString keyword.operator.arrow.ipmt -> srcTokArrow (incl. --::P--, --::L--, --::X--, --::N--) storage.type.node.ipmt -> srcTokTypeMarker (::e / ::t / ::c / ::a / ::tip) (default node text) -> srcTokDefault
Index ¶
- Constants
- func ApplyMarkers(lines []string, blocks []BlockResult) []string
- func EmbedMeta(svg []byte, meta Meta) []byte
- func EqualExceptGeneratedBy(a, b []byte) bool
- func HashIPMT(src string) string
- func IsKey(s string) bool
- func IsMarkerCommentLine(line string) bool
- func NormalizeIPMT(src string) string
- func RenderAndWriteSVG(rootAbs, mdAbs string, br BlockResult, generatedBy string, forceMeta bool) (bool, error)
- func RenderSVGBytes(rootAbs, mdAbs string, br BlockResult, generatedBy string) ([]byte, error)
- func RenderSourceSVG(content string) ([]byte, error)
- func SVGIsFresh(absPath, srcHash string) (bool, error)
- type AnalyzeOptions
- type BlockKind
- type BlockOutcome
- type BlockResult
- type FileAnalysis
- type Marker
- type Meta
- type SourceBlock
- type SrcReader
- type UnknownAttr
Constants ¶
const HashPrefixLen = 8
HashPrefixLen is the number of hex characters emitted in marker hash attributes. 8 hex chars = 32 bits; per-file collisions are vanishingly rare and the per-block ID is the real correlation key.
const MaxBlanksAroundMarker = 2
MaxBlanksAroundMarker is the upper bound on consecutive blank lines on the "away" side of a marker pair (after the image for pos=after, before the comment for pos=before). Excess blanks accumulate from earlier tool behavior or hand-editing; the cleanup pass shrinks them back to this cap.
Variables ¶
This section is empty.
Functions ¶
func ApplyMarkers ¶
func ApplyMarkers(lines []string, blocks []BlockResult) []string
ApplyMarkers rewrites lines so each block has its computed two-line marker, and returns the new line slice. Iterates back-to-front so insertions don't shift earlier indices.
Placement honors br.NewMarker.Pos:
- "" or "after" (default): marker goes immediately below AnchorLine (no blank between the closing fence and the comment line).
- "before": marker goes immediately above OpenLine (no blank between the image line and the opening fence).
On every run the tool also (a) strips one stale blank line between the fence and the marker (legacy from earlier versions of the code) and (b) caps consecutive blank lines on the away-side of the marker at MaxBlanksAroundMarker. Both cleanups keep refresh runs converging on a canonical layout without requiring users to hand-edit existing files.
Behavior per outcome:
- Unterminated / Malformed / MissingSrc: line slice unchanged.
- OK: marker text unchanged, but stale adjacent blank lines are stripped.
- Rehash / Rerender (marker present): rewrite both marker lines in place; strip stale adjacent blanks.
- InsertMarker / Rerender (marker absent): insert "comment + image" adjacent to the chosen edge of the block. No leading or trailing blank.
func EmbedMeta ¶
EmbedMeta returns svg with an XML comment carrying meta inserted just after the opening <svg ...> tag. Any pre-existing ipm-svg meta comment is replaced. The function does not validate or re-emit other svg content; it operates on raw bytes so it survives whatever ipmsvg-gen produced.
func EqualExceptGeneratedBy ¶
EqualExceptGeneratedBy reports whether SVG byte slices a and b are identical once the embedded `generated-by=<tool@version>` provenance field is disregarded. A re-render that changes only which tool/version produced the artifact (e.g. ipm-rpc@… → md-embed@dev) therefore compares equal, letting callers leave the file untouched instead of churning version control.
func HashIPMT ¶
HashIPMT returns the HashPrefixLen-character lowercase-hex sha256 prefix of the normalized source.
func IsMarkerCommentLine ¶
IsMarkerCommentLine reports whether line is the comment portion of a marker. Cheap pre-filter; callers still need ParseMarker (with the following line) for the full structure.
func NormalizeIPMT ¶
NormalizeIPMT canonicalizes an ipmt source so cosmetic edits (trailing whitespace, blank lines at the end, CRLF) do not change the hash. The normalized form is what HashIPMT consumes; the same normalization runs in the in-place embed tool and the vscode extension, so they agree byte-for-byte.
func RenderAndWriteSVG ¶
func RenderAndWriteSVG(rootAbs, mdAbs string, br BlockResult, generatedBy string, forceMeta bool) (bool, error)
RenderAndWriteSVG renders br's ipmt content to an SVG file at br.SVGPath and reports whether it actually wrote the file.
When forceMeta is false (the usual case) and the freshly rendered bytes differ from the existing file ONLY in the `generated-by` provenance field, the write is skipped and (false, nil) is returned — re-rendering with a different tool/version must not churn version control. Pass forceMeta=true to rewrite regardless (e.g. to deliberately restamp provenance across a tree).
Lifted out of cmd/md-embed so cmd/ipm-rpc can call the same code path when serving workspace/executeCommand "ipm.embed".
func RenderSVGBytes ¶
func RenderSVGBytes(rootAbs, mdAbs string, br BlockResult, generatedBy string) ([]byte, error)
RenderSVGBytes renders br's ipmt content to an SVG byte slice with the provenance metadata that SVGIsFresh later checks. Does NOT touch the filesystem — used by ipm.embedBuffer (live preview) where the .md hasn't been saved yet and we don't want disk churn.
rootAbs is the workspace root; used only to compute a stable `source-file` value for the embedded meta (forward-slash, repo-root- relative). generatedBy is a free-form identifier (typically "<tool-name>@<version>") that ends up in the meta as well.
func RenderSourceSVG ¶
RenderSourceSVG produces a colour-syntax-highlighted SVG of the given ipmt source. The SVG is self-contained: a single <svg> root with a background <rect>, one <text> element per source line, one <tspan> per token. Width and height are computed from the longest line and the line count using a fixed monospace metric (8 px advance, 18 px line height).
func SVGIsFresh ¶
SVGIsFresh reports whether the on-disk SVG at absPath was rendered from content whose normalized-source hash matches srcHash. Returns (false, nil) when the file is missing; returns an error only for read failures other than ErrNotExist.
Types ¶
type AnalyzeOptions ¶
type AnalyzeOptions struct {
// Root is the absolute repo root. SVG paths are computed relative to it.
Root string
// SVGDir is the directory under Root that holds generated SVGs. Default: "_ipm".
SVGDir string
// SrcReader loads include source files. Default: os.ReadFile.
SrcReader SrcReader
}
AnalyzeOptions configure AnalyzeMarkdown.
type BlockOutcome ¶
type BlockOutcome string
BlockOutcome reports what the tool would do (or did) for one block.
const ( OutcomeOK BlockOutcome = "ok" // marker present, hashes match, SVG fresh OutcomeInsertMarker BlockOutcome = "insert-marker" // no marker present OutcomeRehash BlockOutcome = "rehash" // marker hash differs from source hash OutcomeRerender BlockOutcome = "rerender" // marker hash matches but SVG file missing or stale OutcomeUnterminated BlockOutcome = "unterminated" // visible: source fence is unterminated; report and skip OutcomeMissingSrc BlockOutcome = "missing-src" // include: the sibling .ipmt referenced by src= can't be read OutcomeMalformed BlockOutcome = "malformed" // structurally bad input (e.g. <!-- ipm-include --> without src=) OutcomeNoEmbed BlockOutcome = "no-embed" // `embed=false`: valid but illustrative — not rendered/embedded OutcomeBadMeta BlockOutcome = "bad-meta" // invalid ipmt metadata (misplaced/unknown `# ipmt:` pragma or fence token) )
type BlockResult ¶
type BlockResult struct {
Index int // 1-based positional index across all kinds in source order
Kind BlockKind // visible / include
OpenLine int // first line of the block (opening fence or ipm-include line)
AnchorLine int // line after which an "after"-positioned marker is expected (closing fence, </details>, or ipm-include line)
MarkerLine int // line of existing marker comment; -1 if absent
OldMarker Marker
NewMarker Marker
SVGPath string // absolute path of the SVG the tool would (re)write
// RenamedFromSVGPath is the absolute path of the SVG this block referenced
// under a previous id, now orphaned because the block was re-keyed (e.g. a
// duplicated id resolved to its own key). The writer removes it after the
// new SVGPath is written. Empty when no rename occurred.
RenamedFromSVGPath string
SrcHash string // hash of the normalized source content
Content string // ipmt source content (already loaded for all kinds)
Meta []string // fence metadata after "ipmt" (e.g. ["unresolved"]); visible blocks only
Outcome BlockOutcome
SkipReason string // populated when Outcome is Unterminated / MissingSrc / Malformed
// Include-specific: absolute path of the sibling .ipmt file referenced
// by the include line (resolved against the .md file's directory).
IncludeSrcAbs string
}
BlockResult is one block's per-analysis verdict.
type FileAnalysis ¶
type FileAnalysis struct {
Path string // absolute path of the source .md
Lines []string
Blocks []BlockResult
}
FileAnalysis is the per-file result of AnalyzeMarkdown.
func AnalyzeMarkdown ¶
func AnalyzeMarkdown(absPath string, mdText string, opts AnalyzeOptions) (FileAnalysis, error)
AnalyzeMarkdown reads the .md file at absPath and produces a FileAnalysis describing every block (visible, include) and what action the processor needs to take. Does no rendering; touches the filesystem only via opts.SrcReader (for include sources).
func (FileAnalysis) HasIPMT ¶
func (a FileAnalysis) HasIPMT() bool
HasIPMT reports whether the file contains any ipmt blocks (of any kind).
type Marker ¶
type Marker struct {
ID string // e.g. "01"; ties to the SVG filename and to the source block within the file
Hash string // 8-char sha256 prefix of the normalized ipmt source
Path string // optional override for the SVG location (relative to the .md file)
Pos string // "" (= after, default) or "before"; where the marker pair sits relative to the block
ImageAlt string // alt text inside the image link; "" by default
ImagePath string // path inside the image link; usually equals Path when Path is set
// Unknown holds attributes the parser did not recognize, in their
// original encounter order. FormatLines emits them at the tail of the
// known-attribute list so a round-trip preserves both the values and
// the relative ordering of known→unknown.
Unknown []UnknownAttr
}
Marker is a parsed two-line marker (a `<!-- ipm-svg ... -->` line followed by an `` line).
Unknown attributes are preserved in Unknown to support forward compatibility: a future version of the toolchain may add new marker attributes, and an older parser should round-trip them faithfully rather than dropping them. See the schema-version rationale in the design notes.
func ParseMarker ¶
ParseMarker parses a two-line marker. Pass the candidate comment line and the line that immediately follows it. Returns ok=false unless both lines match the expected shapes and the comment carries at least id= and hash=.
func (Marker) AttrsString ¶
AttrsString is a helper for tests / debug output; it returns the attributes of m as a deterministic "k=v k=v" string.
func (Marker) Format ¶
Format renders the marker as the two lines joined with a newline. Useful for diff-style output and for tests; callers that need to splice into a line slice should prefer FormatLines.
func (Marker) FormatLines ¶
FormatLines renders the marker as a two-element slice — [comment, image] — suitable for inserting into a `[]string` of file lines. Known attribute order is fixed (id, hash, path, pos) so output is stable across runs; unrecognized attributes (m.Unknown) follow in the order they were originally parsed.
type Meta ¶
type Meta struct {
Hash string // matches Marker.Hash in the .md
SourceID string // matches Marker.ID
SourceFile string // forward-slash, repo-root-relative path of the source .md
GeneratedBy string // tool name + version, e.g. "md-embed@dev"
}
Meta describes the per-SVG metadata embedded in the generated artifact so a refresh tool can detect drift, orphaned files, and renderer-version staleness without parsing the source `.md` again.
func ExtractMeta ¶
ExtractMeta scans svg for an embedded ipm-svg meta comment and parses it. Returns the zero Meta and ok=false if no comment is present.
type SourceBlock ¶
type SourceBlock struct {
Kind BlockKind
// OpenLine is the first line of the block — the opening “`ipmt fence
// for visible blocks, the `<!-- ipm-include` line for include blocks.
// A "before"-positioned marker pair is searched for immediately above
// this line.
OpenLine int
// AnchorLine is the line right after which an "after"-positioned marker
// is expected. For visible blocks it is the closing fence (or </details>
// when the fence is wrapped). For include blocks it equals OpenLine —
// the include line itself is the anchor.
AnchorLine int
// Content is the ipmt source text (LF-joined, no trailing newline). The
// scanner sets it for KindVisible from the lines between the fences. For
// KindInclude the scanner leaves it empty and AnalyzeMarkdown reads the
// referenced file.
Content string
// Meta holds the space-separated tokens after "ipmt" in the opening fence
// info-string (e.g. “`ipmt unresolved -> ["unresolved"]). Visible blocks
// only; empty for a bare “`ipmt fence and for include blocks.
Meta []string
// Existing marker location and parsed contents, if a marker is already present.
HasMarker bool
MarkerLine int
Marker Marker
// Include-specific: the path written on the `<!-- ipm-include src=... -->`
// line (relative to the .md file's directory), plus an optional explicit
// id= attribute. Empty for visible blocks.
SrcPathRel string
ExplicitID string
// Skip is true for blocks the scanner found but cannot process (e.g.
// unterminated visible fence). AnalyzeMarkdown turns these into
// OutcomeUnterminated.
Skip bool
SkipReason string
}
SourceBlock is what the scanner produces. It carries enough information for AnalyzeMarkdown to locate the marker line that should sit just after it (default) or just before it (when the author has placed the marker pair above the block and set `pos=before`).
func ScanBlocks ¶
func ScanBlocks(text string) (lines []string, blocks []SourceBlock)
ScanBlocks walks lines (LF-normalized) once and finds every visible fence and every <!-- ipm-include --> line, in source order. It returns the line slice (the same one ApplyMarkers operates on) plus the blocks.
Fence-aware (see pkg/markdown/fence.go): a non-ipmt fenced block — e.g. a ````md documentation example — is skipped wholesale, so a ```ipmt fence or an <!-- ipm-include --> line INSIDE it is literal text and is never embedded. This is what CommonMark renderers (GitHub, the VS Code preview) already do; embedding would insert a marker into the example.
type SrcReader ¶
SrcReader resolves an include `src=` path (already converted to absolute) to its content. Defaults to os.ReadFile; tests can swap it.
type UnknownAttr ¶
UnknownAttr is a marker attribute the parser did not recognize. Held by Marker so unknown attributes round-trip cleanly.