Semantic markup that reads three ways.
SemText is a markup vocabulary for HTML documents. You write what the content is — a fact, a step, a caveat, a property — and the same file renders as a styled interactive page in a browser, extracts as structured records for a language model, and prints as annotated text in a terminal.
A document is one standalone .html file. Double-click it.
There is no server, no build step and no framework to install.
See it work Read the spec Open the demo
This page is itself a SemText document. Everything below is the real vocabulary, rendered by the real bundles. View source.
Why this exists
One document, three readers who want different things.
A rich document usually ends up as three artifacts that drift: a page for people, a JSON export for tooling, and a plain-text dump for anything without a browser. SemText's bet is that one file can serve all three, if the markup says what each piece of content means instead of how it should look.
Pick a reader to see what they get from the same file.
The theme CSS styles the vocabulary directly, so a document is readable the moment it opens. Flashcards flip, quizzes score, tabs switch and disclosures expand — driven by roughly 7 KB of vanilla JavaScript with no dependencies and no network calls.
The same file read as XHTML. Attributes qualify every datum:
data-kind says what a thing is,
data-status says where it stands,
.sem-conclusion and .sem-distractor
distinguish an answer from a decoy. A model does not have to infer
the structure, because the structure is written down.
extractText(document) walks the authored markup and
returns annotated lines — one per record, indented by containment,
tagged with ids and kinds. Generated chrome is skipped, so the
output does not depend on whether scripts ran or which view is on
screen.
The idea, in one control
Change the view. The content does not move.
These four facts are authored once, as
div.sem-fact elements holding a
.sem-statement and a .sem-conclusion.
data-view-as selects how they render. Switching it
rewrites nothing about what they are, and the records a machine reads
stay identical.
Rendering as list. In flashcard view,
click a card to flip it; in quiz view, pick the conclusion.
What a person sees
view-as is a parameter, not an identityWhat a machine reads
sem-facts (#try-deck kind=capability)
sem-fact (#f-view-as kind=rule): view-as is a parameter, not an identity :: A fact is a fact in every view; the attribute only selects a rendering.
distractors: Each view is a different element type.
sem-fact (#f-light-dom kind=rule): Components render into light DOM :: Content stays in the page, so find-in-page, copy and plain CSS all still reach it.
distractors: Content moves into a shadow root for encapsulation.
sem-fact (#f-invariant kind=rule): Extraction reads the authored document :: Generated chrome is skipped, so records do not depend on the view on screen.
distractors: Extraction reads whatever is currently rendered.
sem-fact (#f-no-build kind=rule): Authoring needs no build step :: Two stylesheets and one script tag in a plain HTML file.
distractors: A bundler compiles the vocabulary down to HTML.
Output of SemTextExtract.extractText() over the deck
above, authored into the page so it is here with scripts off too.
Degradation
Three tiers, and the bottom one is the contract.
Every hide rule in the vocabulary CSS is gated on an enhancement marker that only a script sets. With no script, nothing is hidden and the document reads as prose. Each tier above that adds behaviour without moving content.
data-sem-upgraded, drop the fallback marker and take
over in place. Content is never re-parented into a shadow root.
The notes further down this page are <sem-note> custom
elements running tier 3. The deck above is running tier 2.
Quick start
What you actually write.
A SemText document is an HTML file. The vocabulary is classes on plain elements, so any editor, template language or generator that emits HTML can emit it.
<!doctype html>
<html lang="en" data-sem-theme="minimal-tech-light">
<head>
<meta charset="utf-8">
<title>Auth notes</title>
<link rel="stylesheet" href="semtext/themes/_vocabulary.css">
<link rel="stylesheet" href="semtext/themes/minimal-tech-light.css">
<script src="semtext/semtext-fallback.js"></script>
</head>
<body>
<div class="sem-enhanced-document">
<h1>Auth notes</h1>
<div class="sem-note" data-variant="warning">
Refresh tokens rotate per use — each exchange retires the last one.
</div>
<div class="sem-facts" data-view-as="flashcards">
<div class="sem-fact" id="f-jwt" data-kind="concept">
<div class="sem-statement">JWTs rotate per session</div>
<div class="sem-conclusion">Short-lived access tokens; the refresh grant issues a new pair.</div>
<div class="sem-distractor">Store the token in localStorage for durability.</div>
</div>
</div>
</div>
</body>
</html>
Save it, double-click it. That is the whole workflow.
Adding it to a project
themes/_vocabulary.css and one theme file next to
your document, and set data-sem-theme on
<html> to that theme's name.
semtext-fallback.js. Link it, or paste it inline for
a document you intend to send as a single file.
sem-* classes where interactivity
or machine-readability pays. Ordinary HTML stays ordinary HTML.
semtext.js for the Lit tier, and
semtext-extract.js where a tool needs the records.
Surface
Everything that ships.
Three independent classic scripts, each installing one global, plus two stylesheets. They are separate on purpose: the fallback tier has to run in a document that never loads Lit.
SemText · 24.4 KB minified, 8.9 KB gzippedSemTextFallback · 7.1 KB minified, 2.6 KB gzippedSemTextExtract · 6.6 KB minified, 2.5 KB gzippedsem-* classes. Reads --sem-* tokens, declares none.prefers-color-scheme block. A new theme is a copy with new values.The vocabulary
Fourteen element types mint records:
sem-agent, sem-note, sem-facts
and sem-fact, sem-details and
sem-detail, sem-procedure and
sem-step, sem-properties and
sem-property, sem-views and
sem-view, sem-reveal, and
sem-progress. This page mints records for eleven of
them; the three it leaves out are sem-details,
sem-detail and sem-progress, because nothing
here is an occlusion exercise and there is no honest number to meter.
A sem-agent block, above: document-level metadata aimed at
a model, carried by the same file a person is reading. It is inert to a
browser and it is a record to an extractor.
Scope