A MarkEdit extension that finds raw HTML in a Markdown file which silently breaks rendering, and tells you exactly where the rendered output stops.
Generated Markdown — security reports, API docs, anything written by a tool — tends to contain angle-bracket placeholders in prose:
Iframe embedding confirmed: <iframe src={iframeSrc}> loads the builder origin directly.That is not text. Per the CommonMark grammar it is a valid HTML tag, and iframe is a
raw-text element: the HTML parser stops recognising markup inside it and keeps going to
the end of the file looking for </iframe>. Everything after that line is absorbed as
inert fallback content and renders as nothing at all — no error, no warning, no
visible gap. In the document that prompted this extension, one such line on line 677
silently discarded the remaining 3,845 lines, 85% of the report.
The editor is no help here, because the source is perfectly valid Markdown. Only the rendered output is destroyed.
| Rule | Severity | Effect on the rendered document |
|---|---|---|
unclosed-raw-text-element |
critical | <iframe>, <script>, <style>, <textarea>, <title>, <noscript>, <template>, <xmp> and friends swallow the rest of the file. |
unclosed-comment |
critical | A <!-- with no --> absorbs everything after it. |
unclosed-code-fence |
critical | The remainder of the file becomes one code block. |
unclosed-element |
error | <div>, <details>, <a> … stay open, so the rest of the document is re-parented inside them. Visible, but mis-nested. |
stray-closing-tag |
error | A closing tag with no opener. The element you meant to close stays open. |
mismatched-nesting |
error | Overlapping rather than nested tags; the renderer drops one. |
text-parsed-as-html |
warning | <ownerId>, <victim ref> and similar are read as unknown HTML tags. The placeholder vanishes from the output, leaving a gap where a value should be. |
Severities are not guesses. tools/deriveTables.mjs measures them: it renders
Intro <tag> tail… for every candidate element, parses the result with a
spec-compliant HTML parser, and reports where a marker further down the document
actually lands. src/htmlSpec.ts records that outcome.
False positives would make the tool useless on exactly the documents it is meant for, so tag detection follows CommonMark's raw-HTML grammar exactly, via the same Lezer parser the editor uses:
- Fenced code blocks and inline code spans are never scanned.
{"name":"<a global admin's email>"}is left alone — an apostrophe cannot appear in an attribute name, so this is literal text and renders verbatim.- Void elements (
<br>,<hr>,<img>) and self-closing tags need no closing tag. - Elements whose end tag HTML makes optional (
<p>,<li>,<td>…) are not reported. - A custom element that is closed somewhere (
<my-widget>x</my-widget>) is deliberate; one that never closes (<victim-hid>) is a placeholder.
npm install
npm run install-script # builds, then copies into MarkEdit's scripts directoryThen restart MarkEdit — user scripts are injected when a document window is created, so a running instance will not pick up the change.
The script is installed to:
~/Library/Containers/app.cyan.markedit/Data/Documents/scripts/markedit-html-validate.js
To uninstall, delete that file and restart.
There are two ways to run a check. Opening a document checks it automatically, and Extensions ▸ HTML Validate ▸ Validate Document checks whatever is in front of you right now — use that after fixing something.
| Command | Shortcut | |
|---|---|---|
| Validate Document | ⇧⌘H | Check now, and always show the summary |
| Next Problem | ⌃⌘H | Cycle through findings, worst first |
| Show Problem List | Jump-to menu of every finding | |
| Clear Highlights | Remove the underlines |
Unlike the automatic check, Validate Document always reports — including a plain
"no problems found" — and ignores the maxAutoCheckBytes size limit.
The document is checked immediately. If anything critical is found you get an alert naming the line, what it does, and how much of the document is lost:
This document will not render fully
Line 677: <iframe src={iframeSrc}> is never closed, and iframe content is not
parsed as HTML.
Every line after this point is absorbed into the element and renders as nothing
at all. That is 3,845 of 4,522 lines — about 85% of the document.
Also found: 11 placeholders read as HTML tags.
Errors and warnings do not interrupt you — they are underlined in the editor (red wavy for critical, amber for errors, dotted yellow for warnings).
While you type, underlines refresh 400 ms after you stop. No modal ever appears from typing — only from opening a document, or from asking.
Add to MarkEdit's settings.json, under your own htmlValidate key:
{
"htmlValidate": {
"alertOnOpen": true,
"validateWhileTyping": true,
"maxAutoCheckBytes": 4000000
}
}alertOnOpen— show the modal when a document you open will not render fully. Underlines still appear when off.validateWhileTyping— keep underlines current as you edit.maxAutoCheckBytes— skip the automatic checks above this size. The manual command always runs regardless.
The same validator runs as a command-line check, so generated Markdown can be caught before it reaches an editor at all. It exits non-zero on any critical or error finding:
node tools/check.mjs report.md
npm run check -- docs/*.mdnpm run typecheck # tsc --noEmit
npm test # validator unit tests + bundle integration tests
npm run build # single-file CJS bundle into dist/
npm run derive-tables # re-measure the element behaviour tables
npm run reload # quit and relaunch MarkEditnpm test covers two layers. The unit tests check the validator's findings against
independent ground truth: every claim like "this content is invisible" is verified by
rendering the Markdown and inspecting the resulting DOM (tools/renderedVisibility.mjs),
rather than by trusting the validator's own reasoning. The integration tests load the
built bundle exactly as MarkEdit does — as CommonJS with an injected require handing
back real CodeMirror modules — and drive the menu actions against a stub host, so
decoration ranges are replayed through a real StateField that would throw if they were
malformed.
Community extensions are listed at
markedit-app.github.io/extensions, fed by
the MarkEdit-app/extensions repository. An
entry is a single JSON file; registry/markedit-html-validate.json here is ready to
submit once the placeholders are filled in.
Review is about provenance and integrity — the source is identifiable, the URL is reachable over HTTPS, the hash matches, and the extension does what it claims. Taste and completeness are not judged.
-
Push this project to a public GitHub repository, and set
authorandhomepageinregistry/markedit-html-validate.jsonaccordingly.homepageis just the repository URL — every extension in the registry points at a GitHub repo, and this README is what a visitor lands on. -
Cut a
v1.0.0release and attachdist/markedit-html-validate.jsas an asset. A Release asset is preferred over a raw file URL, because downloads are then counted. -
Re-hash the exact bytes you uploaded and update
sha256:npm run sha256
-
Fork
MarkEdit-app/extensions, copy the JSON toextensions/markedit-html-validate.json, and open a pull request. CI checks the schema, thatidmatches the filename, and that the hash matches the fetched bytes.
Leave addedDate and date out — the publish workflow adds them after merge. Later
versions are appended to versions, newest first; only the newest five are kept.
| Path | |
|---|---|
main.ts |
Entry point: menu, lifecycle, settings |
src/validator.ts |
The analysis. Pure, no MarkEdit dependency |
src/htmlSpec.ts |
Measured element behaviour tables |
src/decorations.ts |
CodeMirror underlines and reveal-range |
src/report.ts |
Alert and problem-list wording |
tools/check.mjs |
Command-line front end |
tools/deriveTables.mjs |
Regenerates the tables in htmlSpec.ts |
tools/renderedVisibility.mjs |
Independent ground truth for the tests |
@lezer/markdown, @codemirror/* and markedit-api are external in the bundle: MarkEdit
provides them at runtime, so the extension shares the editor's own module instances. The
validator parses the document itself rather than reading the editor's live syntax tree,
because CodeMirror parses lazily and would leave a large document's tail unexamined.
MIT. See LICENSE.