Edit Markdown as living structure — the raw markers never leave the text. One
editor engine projects headings, lists, tables, and inline marks as real UI while
##, **, and ` stay present in the document, so every keystroke round-trips
back to plain Markdown byte-for-byte.
This monorepo is the source of truth for the tile family: a shared editor engine, the hosts built on top of it, and the renderers that turn the same Markdown into published pages, cards and embeddable widgets. Every piece of the family's CODE lives here; what does not is a particular deployment's data and deploy configuration, which belongs to whoever runs it.
Most rich editors throw the Markdown away and keep a model. tile keeps the Markdown
as the model: the text stays authoritative, and the editor decorates it in place —
headings grow, tables snap to a grid, inline **bold** renders while its markers
hide via CSS — without ever rewriting the underlying characters. Read the text back
with getText() and you get exactly what a plain editor would. That "structure on
top of an unmodified text substrate" is the whole design, and it is why the same
engine can drive very different hosts.
Three editor hosts ship from here today — two Obsidian plugins and a macOS app — alongside the renderers described under How it works:
- tugtile — a card table (kanban) for your Markdown notes: tug tiles to reorder, lanes with WIP limits, and it reads your existing kanban-style boards. CJK-friendly.
- marktile — a Markdown editor where the markers never hide: headings grow while
##stays put. Opens any.mdfile, not just boards. Pairs with tugtile. CJK-friendly.
Both are in the official Obsidian community-plugins directory.
- marktile for macOS (
hosts/mac) — the same editor as a document app: double-click a.mdin Finder, no vault involved. AnNSDocument+WKWebViewshell around the same engine, built without Xcode. Not yet signed; see its README for what has and has not been verified.
Engine ⇄ host layering. The engine (packages/core/editor-core.js) is the single
source of the actual editor code — mountEditor, highlighting, list/table/TOC logic,
image paste. It has no hard Obsidian import: it calls a handful of DOM-sugar helpers
(createEl, setIcon, Modal, Platform) that a host provides. Inside Obsidian the
app injects them; outside Obsidian a small shim supplies them. The core is never forked
per host.
packages/core— the editor engine (SSOT). What it knows about markdown, where it stands against CommonMark, and what it still gets wrong:MARKDOWN.md.packages/cssmd— the shared inline-mark primitive (**bold**/*italic*/`code`rendered with the raw markers hidden via one CSS rule). The engine delegates to it so there is one implementation, not a copy per consumer.packages/tugtile— the board model:board-core.js, the platform-free half of the kanban host, sliced out of the plugin source byscripts/build-board-core.sh.hosts/obsidian/{tugtile,marktile}— the two Obsidian plugins;hosts/web/*the browser surfaces;hosts/macthe macOS host, anNSDocument+WKWebViewshell whose engine is copied in at build time rather than vendored, so there is no second copy to drift.
-
packages/sitetile— the site renderer: an Astro build that turns a directory of Markdown into a published multilingual site, plus the page/theme model (site-core.js) it round-trips.packages/pagetileis the long-form reader core andpackages/pwathe installable shell. -
packages/cardtile— REEF with Card: a one-page Card as Markdown, its multi-tenant edge worker, and two sandbox editors (w/a modal,w2/the tugtile board as an editing desktop). It importspackages/tugtile,packages/sitetileandpackages/cssmddirectly — the Card model is a projection of the board model, not a second one. -
packages/dynamic-corals— edge-rendered widgets that install unmodified into a sitetile page or a Card: a shop, an events list, an animated QR, a sponsor form, a drawer, an inbox bubble. Vanilla JS and the nativefetch/DOM only, configured entirely fromdata-*attributes, so the same file drops into a static build or a framework component without a wrapper.packages/flowtileis the flow/diagram core.🔴 No hostname and no published artifact lives here. A coral's source carries a NEUTRAL default marked
/*coral-default:<key>*/, and a deployment substitutes its own at build time (build.mjs --defaults <file>); the registry it publishes into — the immutable versions and the mutable channel table — belongs to that deployment too, andregistry/registry-worker.mjsimports the table from a specifier nothing here resolves. Seepackages/dynamic-corals/registry/README.mdfor why an unresolvable import is the safe design and a fallback would not be.
Each plugin builds to a single main.js by inlining the engine, cssmd, the shared
i18n/*.json strings, and SortableJS (drag-and-drop) — so what a user installs is one
self-contained file. Obsidian is the first host; the engine is also emitted as a
platform-agnostic ES module (tile-core.js) for a browser host that loads the shim
first.
Localized in en-US, ja-JP, ko-KR, zh-TW — locale strings live in i18n/*.json and
are injected at build time.
The engine is framework-agnostic — drop it into any page with a small host shim. A
runnable demo lives at hosts/web/example/index.html; the shape:
<div id="app"></div>
<script type="module">
import '../packages/core/obsidian-shim.js'; // browser equivalents of the DOM-sugar the engine calls
import { mountEditor } from './tile-core.js'; // the engine
import { makeWebHost } from './host.js'; // a tiny host — see packages/core/web-host.js
mountEditor(document.getElementById('app'), {
text: '## Hello\n\n- edit me',
onChange() { console.log('changed'); },
}, makeWebHost());
</script>obsidian-shim.js supplies plain-browser equivalents (Lucide icons as inline SVG, a
no-op Modal, HTMLElement.prototype sugar) so the core runs byte-identical in a bare
page — no if (isObsidian) branches. The shim's name just reflects where that API shape
came from.
This monorepo is the source. The two Obsidian plugins ship as thin publish mirrors (Obsidian requires one repo = one plugin, so nobody hand-edits those):
| Artifact | Published as | What it is |
|---|---|---|
| tugtile plugin | CVERInc/obsidian-tugtile (MIT) |
Obsidian community plugin |
| marktile plugin | CVERInc/obsidian-marktile (MIT) |
Obsidian community plugin |
scripts/publish.sh assembles a plugin payload (main.js + manifest.json +
styles.css + versions.json + LICENSE) and builds fresh from this repo — the
publish mirrors can never drift into their own development line.
The editor engine needs no separate repo: it lives here as
packages/core/editor-core.js (source), emitted to dist/editor-core.js (a
platform-agnostic ES module). Drop it into any page with the small host shim — see
Use the engine below.
Before you move or rename a file, check PUBLIC-PATHS.json. Some
paths here are fetched by name from outside this repo — nothing in this tree imports
Sortable.min.js, but two other builds download it by that exact string. The manifest
lists them with what breaks, and test/public-paths.test.cjs fails the moment one goes
missing, so the rename goes red here rather than in someone else's deploy log next week.
Requirements: Node, python3, bash (the builds inline sources via a short Python step).
bash scripts/test.shThat single entry point runs syntax checks, validates the i18n JSON, builds both
plugins and the tile-core emit, asserts the committed build artifacts still equal a
fresh build, and runs the full Node test suite. The same script backs the hooks/pre-push
git hook and CI (.github/workflows/ci.yml), so local and CI can never disagree.
A handful of tests ask about a deployment's published artifacts — whether a coral's channel
table, the artifact on disk and the coral's own version agree. There is no deployment here, so they
skip by name and say what they did not look at. Point CORAL_REGISTRY at a registry directory
to run them: "I could not look" and "I looked and it is fine" must never print the same thing.
docs/experiments/ holds one-off investigations kept for their measurements. Some of their inputs
(screenshots of real pages) are deliberately not in this repo; each README says so where a reader
would otherwise read an empty directory as an empty result.
Edit the *.src.js and packages/core/editor-core.js sources — never the generated
main.js / dist/editor-core.js; those are overwritten on the next build and the
freshness check fails if a committed artifact is stale.
To activate the pre-push hook once: git config core.hooksPath hooks.
Issues and pull requests are welcome. A few house rules:
- Change source, not generated artifacts, and run
bash scripts/test.shbefore pushing. - Translations are just JSON: edit
i18n/<locale>.json(the four existing locales are the template) — no code change needed to improve wording. - Keep the round-trip guarantee: the editor must never alter the underlying Markdown text.
MIT — see LICENSE. Drag-and-drop is powered by SortableJS (MIT).