Skip to content

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 01 Sep 02:10
· 28 commits to main since this release
v0.10.0
e4b427f

Release Notes - Version 0.10.0

Release Date: September 1, 2026

Summary

This major update introduces cross-axis layout attributes, allowing wrappers to accept their complementary axis, and adds comprehensive text-alignment attributes across core components. It delivers powerful export features, including self-contained HTML generation with embedded images, Obsidian HTML cascades, and direct PDF printing. It also brings pack-declared attributes directly into directive completion and hover documentation, adds a new fit width preset, and improves nested container fence management.

Added

  • A layout wrapper now takes the other axis as an attribute (@markii/react, @markii/html, @markii/stdlib): width sizes a block's box and align places that box in the column, and each of the seven wrappers already sets one of those two by its name. It now accepts the other one as an attribute, so :::center{width=fit} and :::fit{align=center} say the same thing and render as ONE element carrying both classes (mk-layout mk-align-center mk-width-fit), not two nested wrappers. An attribute for the wrapper's own axis is ignored, because the name already decided it: :::center{align=right} is simply centered. Invalid values are ignored as before. Nesting two wrappers still works and still means the same thing. Directive completion follows the same rule: an alignment wrapper offers width only, a width wrapper offers align only.
  • text aligns the content inside a component (@markii/stdlib, @markii/react, @markii/html): row, cell, card, and callout accept text=left|center|right. On a row it reaches every cell; a cell with its own text overrides it, and an alignment wrapper written inside a cell wins over both. text is an ordinary per-component attribute, not a reserved layout key: it reaches the component like any other. It is rendered through one new class set (mk-text-left/mk-text-center/mk-text-right) defined once in doc.css, and it is offered by directive completion and hover on those four components.
  • @markii/stdlib: layoutWrapperAxis, otherLayoutAxis, and the LayoutAxis type: the one classification of which axis each wrapper name decides, plus TEXT_ALIGN_PRESETS, TEXT_ALIGN_ATTRIBUTE, and TEXT_ALIGN_COMPONENTS for the text attribute. Both renderers and the completion layer read these instead of keeping their own copies.
  • **@markii/react: RegistryEntry.layout and the layoutClassName prop; @markii/html: HtmlRegistryEntry.layout and HtmlRenderContext.layoutClassName**: An entry that declares layout is a scope that already sets that axis by its name: the renderer drops that axis's attribute, resolves the other one, and hands the resulting class to the component instead of wrapping it in a second <div>. A component still never receives width or align among its attributes. Every entry without layout behaves exactly as before.
  • Export a note as a self-contained HTML file (VS Code extension, Obsidian plugin): Markii: Export as HTML… in VS Code asks where to save and writes one .html file; Export Markii note as HTML in Obsidian writes it beside the note in the vault. The file carries the shared doc.css inside it and needs nothing beside it. The note's last run is baked in, so a monitoring note exports with the figures it was showing; a note that has never been run exports with its usual empty states, and both hosts say so. Relative image sources are preserved, so a file written beside its note resolves them the same way the note does.
  • Pack components render in an exported file: When the exporting host has packs loaded, the export is rendered with the same merged registry and value store the preview uses, and each loaded pack's stylesheet is embedded after doc.css in the order the host injects it, so the file matches the preview. Obsidian renders directly; VS Code asks its preview webview and waits a bounded moment for the answer. With no packs loaded, or with no renderer to reach, the export falls back to @markii/html, the static string engine, where a pack directive comes out as the ordinary unknown-component fallback: a labeled box with the author's own content still rendered inside it. Neither outcome is treated as a failure, and the two are distinguishable on each host's diagnostics surface without changing what the notice says.
  • Exported files carry their own images: A local image a note references is embedded in the exported HTML as a data: URI, so the file is self-contained: it opens with its pictures intact on a machine that has never seen the vault or the workspace. Remote http and https sources stay live URLs, since embedding them would change what the page fetches. Each host resolves an image the way its own preview does and reads it under its own jail, so an export can never pull in a file the preview itself could not show. An image above 2 MiB keeps its original path rather than inflating the file, and so does one whose extension the embedder does not recognize, or one that could not be read. Every such case is named on the host's diagnostics surface with its file and size. None of them is a failure and none of them reaches the notice. The Obsidian PDF export benefits automatically, since it prints exactly this document.
  • Export a linked set of notes as one archive (Obsidian plugin): Export Markii note as HTML cascade follows the links out of the active note, both wikilinks and markdown links that resolve to notes in the vault, exports every note it reaches as its own HTML file with images embedded, and writes the whole set as one zip beside the root note. Links between exported notes are rewritten to point at the sibling files, so the archive is navigable offline; a link to a note outside the exported set, or to something that is not a note, is left exactly as written. The walk detects cycles and stops at a depth and a note-count bound, and says on the diagnostics surface which bound stopped it. Cascade to PDF is not part of this release.
  • Export a note straight to PDF (Obsidian plugin): Export Markii note as PDF prints exactly the document the HTML export would have written, through Electron's printToPDF in a hidden window, and writes the .pdf beside the note. Where that is unavailable, on a device with no reachable Electron window or a vault with no filesystem path, the command writes the HTML file instead and says which of the two happened. The reason goes to the console, this host's diagnostics surface, never into the notice.
  • **@markii/pack: a component can declare its attributes in pack.json**: The object form of a components entry takes an optional attributes list, each entry carrying a name plus an optional description, required flag, closed values set, and default. The field is optional and every existing manifest keeps parsing unchanged, so this is backward compatible. parsePackManifest validates the list strictly and rejects a manifest that gets it wrong: a bad attribute name, a duplicate name, a non-boolean required, a values list that is not a non-empty array of non-empty strings, or a default outside its own values. An unknown key inside an attribute entry stays a forward-compatible warning. resolvePackComponent normalizes the same field leniently for callers reading a manifest that never went through validation, dropping what it cannot make sense of. New exports: the PackComponentAttribute type, PACK_ATTRIBUTE_NAME_PATTERN, and isPackAttributeName.
  • divider takes a label-align attribute (@markii/stdlib, @markii/react, @markii/html): ::divider{label="Part 2" label-align=left} positions the label along the rule instead of centering it. The value is one of left, center, or right, and an absent or unrecognized value falls back to center, so every existing divider renders exactly as it did. It is a component-scoped attribute, distinct from the reserved width and align layout attributes, which are unchanged. Both renderers emit the same mk-divider--label-left / mk-divider--label-right modifier class, and only for the non-default values, so doc.css still covers both.
  • A fit width preset (@markii/stdlib, @markii/react, @markii/html): width=fit shrinks a block to its own content instead of filling the column, and the matching :::fit wrapper does the same for plain markdown that has no attributes to write on. The width scale now reads fit, narrow, normal, wide, full, and WIDTH_PRESETS is listed in that order, which is also the order a completion popup offers the values in. Pairing it with an alignment is the point: {width=fit align=right} hugs the content and sits right, because a box narrower than its container is what gives the alignment margins room to work. Both renderers pick up mk-width-fit from the shared preset list with no class map of their own to edit, and doc.css carries the one new rule.
  • Reworded the reserved align attribute's documentation (@markii/stdlib): Completion and hover now say that align places the block when it is narrower than the page, and that it should be paired with a width. The behavior is unchanged.
  • Inserting a container inside a container lengthens the enclosing fences (VS Code extension, Obsidian plugin): Nesting needs the outer fence pair to carry more colons than the inner one, which until now the author had to widen by hand. Both hosts now do it in two places only: the Insert Component command, and accepting a container from the completion popup. Never while typing. The rewrite lands in the same undoable edit as the insertion, and cascades outward only as far as it has to, leaving an outer pair alone once it is already long enough. It is quiet either way: a document whose fences do not pair cleanly from the top, or that has a ::: line inside a code block or an unterminated fence, is left untouched and the insertion proceeds as before. The shared scan and the edit computation live in the private host package, so both hosts run the same logic.
  • A preview width setting (VS Code extension, Obsidian plugin): The preview's text column can now be widened. In VS Code the new markii.previewWidth setting takes normal, the 48rem reading column the preview has always used, wide for 64rem, the same measure a width=wide block gets, or full for no cap at all with slightly wider gutters. It is read when a preview opens, so change it and reopen the preview to apply. In Obsidian the same three values live in the plugin's settings tab under Preview width, applied to open previews immediately. Obsidian already gives the preview the whole pane, so there wide is the value that introduces a fixed reading column and full only widens the gutters. Both hosts default to normal, which renders exactly as before.
  • Run and Export in the Obsidian preview's header: The Markii preview pane now carries two icons, Run Markii scripts and Export Markii note as HTML, so the two things a reader does with an open note are one click away. PDF, the HTML cascade, Insert component, and Show diagnostics stay in the command palette. The diagnostics icon that used to sit in the preview header was removed with it.
  • Export as HTML in the VS Code editor title menu: Markii: Export as HTML is now contributed to the editor's overflow menu for a .mk.md file, alongside the Open Preview action, so it no longer needs the command palette.

Fixed

  • Images resolve in the Obsidian preview (Obsidian plugin): A note that references a picture sitting next to it, whether as :::figure{src="./photo.png"} or as a plain markdown image, showed a missing file rather than the picture, and a path read from the vault root failed the same way. The preview now rewrites every image source to the URL Obsidian serves the file at, after each render, so a value update or a fresh run keeps the pictures in place. A source is read against the note's own folder first, then through Obsidian's own link resolution, so a shortest-path reference finds its file the way it does everywhere else in the app, and then against the vault root. Percent-encoded names such as my%20image.png resolve too. This is the same reading of a source the HTML and PDF exports already used, now shared between the two so a note previews with the pictures it exports with. A source with a scheme of its own, an https: or data: image, is left exactly as written, and so is one that matches no file in the vault: the image is simply missing, as it was before, and the reason is named once on the console, this host's diagnostics surface.

Changed

  • BREAKING (@markii/react, @markii/html, @markii/stdlib): align on a :::row no longer means "align the content inside the cells": It now means what it means on every other block, placing the row's own box, which a full-width grid has no room to act on, so writing it on a row is a visible no-op. The three doc.css rules that gave it the old meaning (.mk-align-left > .mk-row and its two siblings) are gone. Use text= instead: :::row{cols=4 text=center} does what :::row{cols=4 align=center} used to do. A note that still writes align= on a row keeps rendering, it simply stops centering.
  • The seven layout-wrapper contracts declare one attribute each (@markii/stdlib), the axis their name did not decide, where they previously declared none. cell declares text where it previously declared nothing. row, card, and callout each gain text.
  • resolveLayoutAttributes takes an optional owned-axis argument (@markii/html, and the same internal helper in @markii/react): Existing one-argument calls are unaffected.
  • :::left now declares its own text alignment in doc.css (.mk-layout.mk-align-left). Only a declared value beats an inherited one, and :::left exists to opt one scope back out of an alignment it inherited, such as a cell of a :::row{text=center}.
  • Export confirmations say the same thing on both hosts: The VS Code popup no longer counts the script values baked into the file. Both hosts now name the exported file and nothing more, keeping only the hint that a note with scripts which has never been run exports with empty states. The value count still appears in full on each host's diagnostics surface, where the byte count, the render engine, and the image report already live. A scriptless note is still never told to run itself first.
  • Directive completion and hover now know a pack component's attributes (VS Code extension, Obsidian plugin): Inside the braces of a pack directive, the popup offers the component's own declared attribute names alongside the reserved width and align, marks the required ones, and offers the declared values as attribute-value completions. Hover documentation lists the same attributes, including any declared default. Accepting a pack component's name now pre-fills its required attributes the way a standard component's have always been pre-filled. A pack that declares no attributes behaves exactly as before.

Install

The renderer pulls the rest in as dependencies:

npm install @markii/react@0.10.0

All eight packages, if you want them explicitly:

npm install @markii/core@0.10.0
npm install @markii/stdlib@0.10.0
npm install @markii/bundle@0.10.0
npm install @markii/pack@0.10.0
npm install @markii/runtime@0.10.0
npm install @markii/lua@0.10.0
npm install @markii/react@0.10.0
npm install @markii/html@0.10.0

Downloads

The attached tarballs are the packed npm packages. The names are stable across releases, so the latest of each is always at a fixed URL:

https://github.com/markii-org/markii/releases/latest/download/markii-core.tgz
https://github.com/markii-org/markii/releases/latest/download/markii-stdlib.tgz
https://github.com/markii-org/markii/releases/latest/download/markii-bundle.tgz
https://github.com/markii-org/markii/releases/latest/download/markii-pack.tgz
https://github.com/markii-org/markii/releases/latest/download/markii-runtime.tgz
https://github.com/markii-org/markii/releases/latest/download/markii-lua.tgz
https://github.com/markii-org/markii/releases/latest/download/markii-react.tgz
https://github.com/markii-org/markii/releases/latest/download/markii-html.tgz