This guide takes you from installation to a real source-preserving edit. It assumes you already know basic Go; you do not need to know Marksplice internals.
Marksplice requires Go 1.26 or newer. Install the stable v1.2.0 release explicitly:
go get github.com/zoster81/marksplice@v1.2.0Import the root package:
import "github.com/zoster81/marksplice"From a Marksplice repository checkout:
go run ./examples/inspectThe program reads ../examples/inspect/project-guide.md from disk. That file contains front matter, sections, a task list, a fenced Go block, a table, and links.
Then run the editing example:
go run ./examples/editIt reads ../examples/edit/release-plan.md, prepares several independent edits, combines them, and prints the updated Markdown. It never overwrites the committed fixture.
Marksplice parses bytes, not filenames. Your application decides how the bytes are obtained and where any result is written.
source, err := os.ReadFile("project-guide.md")
if err != nil {
return err
}
doc, err := marksplice.Parse(source)
if err != nil {
return err
}Document is an immutable snapshot of those bytes. Keep the original source if you intend to apply a prepared edit later.
Parse does not read other files, follow URLs, or crawl directories. For an explicit read-only multi-file workflow, the separate workspacefs package can scan or follow Markdown inside a caller-supplied fs.FS under finite resource limits; see Links and workspaces.
Document.Nodes() returns source-ordered public structural nodes. Use a typed accessor when you need details for one kind.
for _, node := range doc.Nodes() {
if node.Kind() != marksplice.KindHeading {
continue
}
heading, ok := doc.Heading(node.ID())
if !ok {
continue
}
text, ok := doc.SourceRange(heading.Range())
if !ok {
return errors.New("heading source is unavailable")
}
fmt.Printf("level=%d text=%s\n", heading.Level(), text)
}Other common views include:
Sections()for heading-governed document sections;Task(id)for task state;Table(id),TableRow(id), andTableCell(id)for tables;FencedBlocks()for complete fenced-container metadata;LinkRelationships()for links, images, references, and autolinks;FrontMatter()for a recognized YAML/TOML document envelope.
See the runnable inspect example for these ideas on one file.
For bounded selection, use QueryNodes or QuerySections. Every query requires a positive limit.
matches, err := doc.QueryNodes(marksplice.NodeQuery{
Kinds: []marksplice.Kind{marksplice.KindHeading},
Limit: 20,
})
if err != nil {
return err
}A Range is a half-open byte range [Start, End) within this exact snapshot. Queries can use Within to stay inside a section or another source region.
The query example finds unfinished tasks only inside one named section.
Suppose you selected a heading and have its NodeID:
change, err := doc.PrepareRenameHeading(headingID, []byte("Release Readiness"))
if err != nil {
return err
}This does not modify doc or source. It returns a ChangeSet: an opaque change prepared for this exact snapshot.
Now apply it to the original bytes:
updated, err := change.Apply(source)
if err != nil {
return err
}Marksplice changes only the source spans owned by that operation. Unrelated bytes are not regenerated through a whole-document renderer.
If source has changed since parsing, Apply returns ErrSourceConflict. Parse the newer bytes and prepare the edit again; do not reuse stale NodeID values or a stale ChangeSet.
Changes prepared from the same snapshot can be combined atomically:
combined, err := doc.ComposeChanges(rename, replaceParagraph, checkTask, updateCell)
if err != nil {
return err
}
updated, err := combined.Apply(source)ComposeChanges rejects overlapping or semantically interacting edits rather than applying them in a guessed order.
The complete edit example combines a heading rename, paragraph replacement, task update, and table-cell update while checking that unrelated source remains present.
Marksplice returns bytes; it does not own filesystem mutation.
if err := os.WriteFile("project-guide.updated.md", updated, 0o644); err != nil {
return err
}For tools that require atomic writes, backups, encoding preservation, authorization, or other filesystem policies, implement those policies outside Marksplice.
Existing-document editing and new-document creation are intentionally separate. Use DocumentBuilder when no original author formatting needs to be preserved:
builder := marksplice.NewDocumentBuilder()
if err := builder.AppendHeadingContent(1, marksplice.TextInline("Release brief")); err != nil {
return err
}
if err := builder.AppendParagraphContent(marksplice.TextInline("Ready for review.")); err != nil {
return err
}
source, err := builder.Markdown()The builder writes deterministic canonical GFM and validates the generated structure before returning it.
Run the larger example:
go run ./examples/buildIt creates front matter, typed inline content, tasks, a table, and fenced shell commands.
Rendering is separate from source-preserving editing and new-document construction. If you explicitly want one deterministic normalized Markdown representation, stream canonical Markdown to any io.Writer:
if err := doc.RenderCanonicalMarkdown(os.Stdout); err != nil {
return err
}Or collect caller-owned bytes:
canonical, err := doc.CanonicalMarkdown()Canonical rendering intentionally normalizes formatting only in the returned export. It leaves the immutable source snapshot untouched, preserves Native semantic meaning after reparsing, and is byte-idempotent when rendered again. Use Prepare... plus ChangeSet.Apply instead when the goal is a small edit that preserves unrelated author formatting. See Render canonical Markdown, or run go run ./examples/render --markdown.
For HTML output, stream a deterministic fragment to any io.Writer:
if err := doc.RenderHTML(os.Stdout, marksplice.DefaultHTMLRenderOptions()); err != nil {
return err
}Or collect caller-owned bytes:
fragment, err := doc.HTML(marksplice.DefaultHTMLRenderOptions())The default policy preserves parser-proven raw HTML, enables the GFM tag filter, and suppresses dangerous URL schemes. Preserved raw HTML is not a sanitizer; use HTMLRawEscape or an application-appropriate downstream sanitizer when untrusted Markdown crosses an HTML security boundary. Rendering performs no URL or asset fetching, command execution, syntax highlighting, or math-engine execution.
For a complete HTML document, use RenderHTMLDocument with DefaultHTMLDocumentOptions. The standalone zero value reuses the fragment safety defaults and maps only exact lower-case title, description, author, and lang fields when they are already unique top-level source-proven simple front-matter scalars. It does not parse arbitrary YAML/TOML; escape-dependent values are omitted rather than guessed, and HTMLMetadataOmit disables front-matter-derived metadata entirely.
When a preview or editor needs to correlate Markdown with rendered HTML, use HTMLWithSourceMap or HTMLDocumentWithSourceMap (or their streaming Render...WithSourceMap forms). Each HTMLSourceMapEntry carries a snapshot-local Markdown byte Range and a byte range in that exact output. The result is semantic-event granular rather than complete coverage, so nested ranges may overlap and synthetic HTML may be unmapped.
See Render HTML for fragment, standalone, metadata, safety, and source-map options. The same tracked render example uses go run ./examples/render for standalone HTML and go run ./examples/render --map for source/output correlations.
| Name | Plain-language meaning |
|---|---|
Document |
Immutable parsed snapshot of one Markdown byte slice |
Node |
Small public summary for one promoted structural item |
NodeID |
Identity valid for that snapshot; use it to ask for typed detail or prepare an operation |
Range |
Exact byte span in that snapshot, with meaning defined by the accessor that returned it |
ChangeSet |
Prepared source-bound edit that can be applied only to the matching original bytes |
DocumentBuilder is the separate mutable value used to create new Markdown.
- User Guide: choose a task and find the right API family.
- Recipes: focused workflows for inspection, editing, creation, canonical Markdown/HTML rendering, source mapping, tables/lists/sections, filesystem workspaces, and extensions.
- Examples: all runnable file-based programs.
- API Reference: exact signatures and exhaustive callable coverage.
- Capabilities: what is supported today and where Marksplice intentionally stops.
For parser architecture, conformance policy, and engineering history, use the maintainer documentation map. Those documents are not required for normal library use.