mdembed

package
v0.4.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

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] -->
![alt](./_ipm/foo/01.ipm.svg)

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

View Source
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.

View Source
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

func EmbedMeta(svg []byte, meta Meta) []byte

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

func EqualExceptGeneratedBy(a, b []byte) bool

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

func HashIPMT(src string) string

HashIPMT returns the HashPrefixLen-character lowercase-hex sha256 prefix of the normalized source.

func IsKey

func IsKey(s string) bool

IsKey reports whether s is a valid 3-char base-36 key.

func IsMarkerCommentLine

func IsMarkerCommentLine(line string) bool

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

func NormalizeIPMT(src string) string

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

func RenderSourceSVG(content string) ([]byte, error)

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

func SVGIsFresh(absPath, srcHash string) (bool, error)

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 BlockKind

type BlockKind string

BlockKind tells where a block's ipmt source was declared.

const (
	KindVisible BlockKind = "visible" // “`ipmt fence in the .md
	KindInclude BlockKind = "include" // <!-- ipm-include src=... --> line referencing a sibling .ipmt
)

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 `![alt](img)` 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

func ParseMarker(commentLine, imageLine string) (Marker, bool)

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

func (m Marker) AttrsString() string

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

func (m Marker) Format() string

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

func (m Marker) FormatLines() [2]string

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

func ExtractMeta(svg []byte) (Meta, bool)

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

type SrcReader func(absPath string) ([]byte, error)

SrcReader resolves an include `src=` path (already converted to absolute) to its content. Defaults to os.ReadFile; tests can swap it.

type UnknownAttr

type UnknownAttr struct {
	Key   string
	Value string
}

UnknownAttr is a marker attribute the parser did not recognize. Held by Marker so unknown attributes round-trip cleanly.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL