Prechádzať zdrojové kódy

Document the CommonMark markdown engine and add a demo section

model.md's Markdown binding section now describes the renderer's actual
behaviour: paragraphs break only on blank lines (single newline = soft break),
delimiter-run emphasis spanning line wraps, setext/ATX headings, tight/loose
lists with wrapped items, lazy block quotes, indented/fenced code blocks with
info strings, link reference definitions, autolinks, hard breaks, backslash
escapes and entity references. The escape-first security posture is stated
explicitly: raw HTML renders as text (the one intentional divergence from the
CommonMark reference output) and link/image destinations are scheme-filtered
to relative/http/https/mailto/ftp. Notes that tables, strikethrough and task
lists are unsupported (not CommonMark).

getting-started.md: the stm-markdown attribute row now reads 'Renders the
value as CommonMark (all source HTML escaped first)', a worked example shows
wrapped-bold and a wrapped bullet item rendering correctly, and a 'Running
the markdown tests' section documents tests/markdown/run.sh (QuickJS vendored
and built on demand, no Node.js required).

frontend-demo: new Markdown section with stm-markdown bound to markdown.body,
a state slot served by build_markdown() in server.php (nowdoc, so the sample
passes through verbatim). The sample exercises the originally-broken patterns
(wrapped bold, soft breaks vs paragraphs, wrapped bullet) plus a code fence,
reference/autolinks, and security probes (raw <script> and a javascript: URL,
both rendered inert).

All documented examples were rendered through the real engine via the qjs
harness to confirm they behave as claimed; js/statum.js is untouched.
saddle-Clanker-Saddle 1 mesiac pred
rodič
commit
d595940ae6
4 zmenil súbory, kde vykonal 149 pridanie a 3 odobranie
  1. 40 0
      frontend-demo/index.html
  2. 36 1
      frontend-demo/server.php
  3. 40 1
      getting-started.md
  4. 33 1
      model.md

+ 40 - 0
frontend-demo/index.html

@@ -120,6 +120,34 @@
       border-radius: 6px;
     }
 
+    /* stm-markdown output */
+    .md {
+      border: 1px solid var(--line);
+      border-radius: 6px;
+      padding: .75rem 1rem;
+      background: #fbfcfd;
+    }
+
+    .md pre {
+      background: #eef2f7;
+      border-radius: 6px;
+      padding: .5rem .75rem;
+      overflow-x: auto;
+    }
+
+    .md code {
+      font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+      font-size: .9em;
+    }
+
+    .md > :first-child {
+      margin-top: 0;
+    }
+
+    .md > :last-child {
+      margin-bottom: 0;
+    }
+
     .counter {
       display: inline-flex;
       align-items: center;
@@ -323,6 +351,18 @@
         </p>
       </section>
 
+      <!--
+        Markdown: stm-markdown renders the state value as CommonMark. The
+        sample deliberately contains the cases the engine is regression-tested
+        for: emphasis spanning a line wrap, soft line breaks (no new paragraph),
+        a wrapped bullet item, a fenced code block, reference + autolinks, and
+        raw HTML / javascript: URLs that must come out inert.
+      -->
+      <section>
+        <h2>Markdown <span class="muted">(stm-markdown · CommonMark, source HTML escaped)</span></h2>
+        <div class="md" stm-markdown="markdown.body">Rendering markdown…</div>
+      </section>
+
       <!--
         Live channel: subscribe/unsubscribe directives drive a realtime
         Server-Sent Events connection; broadcasting pushes a `set` to every

+ 36 - 1
frontend-demo/server.php

@@ -29,7 +29,7 @@ $CATALOG = [
     ['id' => 4, 'name' => 'Gizmo',   'price' => 29.99, 'featured' => true,  'inStock' => true],
 ];
 
-$TYPES = ['counter', 'products', 'cart', 'user', 'notice'];
+$TYPES = ['counter', 'products', 'cart', 'user', 'notice', 'markdown'];
 
 // ---------------------------------------------------------------------------
 // Helpers
@@ -200,6 +200,40 @@ function build_notice(): array
     ];
 }
 
+/**
+ * Markdown sample for the stm-markdown section. Nowdoc (<<<'MD') so the
+ * markdown is passed through verbatim: no PHP interpolation, no escaping.
+ * Deliberately exercises the patterns the renderer is regression-tested for:
+ * emphasis across a line wrap, soft line breaks inside a paragraph, a wrapped
+ * list item, a fenced code block, reference + autolinks, and raw HTML that
+ * must come out as inert text.
+ */
+function build_markdown(): array
+{
+    $body = <<<'MD'
+**This bold span wraps
+onto a second line** without breaking.
+
+These lines form a single
+paragraph: one newline is a soft break, not a new paragraph.
+
+- A bullet whose text
+  continues on the next line.
+- A second bullet with `inline code`.
+
+```vala
+var page = new Html.page ();
+```
+
+A [reference link][docs] and an autolink <https://commonmark.org>.
+
+[docs]: https://commonmark.org "CommonMark"
+
+Raw HTML stays inert: <script>alert(1)</script> and [not a js link](javascript:alert(1)).
+MD;
+    return ['body' => $body];
+}
+
 /** Build the snapshot for a type with its demo scope/options. */
 function snap(string $type): ?array
 {
@@ -209,6 +243,7 @@ function snap(string $type): ?array
         case 'cart':     return make_snapshot('cart', 'session', build_cart(), ['always' => true]);
         case 'user':     return make_snapshot('user', 'session', build_user(), ['stale' => true]);
         case 'notice':   return make_snapshot('notice', 'flow', build_notice());
+        case 'markdown': return make_snapshot('markdown', 'session', build_markdown());
     }
     return null;
 }

+ 40 - 1
getting-started.md

@@ -388,7 +388,7 @@ It auto-initialises on DOMContentLoaded.
 |---|---|
 | `stm-text="counter.value"` | Sets the element's text content from the expression. |
 | `stm-html="cart.summary"` | Sets the element's inner HTML. |
-| `stm-markdown="post.body"` | Renders the value as safe markdown (all source HTML escaped first, then formatted). |
+| `stm-markdown="post.body"` | Renders the value as CommonMark (all source HTML escaped first). |
 | `stm-h-demotion="1"` | Demotes child `<h1>`–`<h5>` tags by N levels. Runs after `stm-html`/`stm-markdown`. |
 | `stm-if="auth != null"` | Shows/hides the element (and its `stm-else-if`/`stm-else` siblings). |
 | `stm-else-if="auth.role == 'admin'"` | Next sibling of `stm-if`/`stm-else-if`. |
@@ -398,6 +398,28 @@ It auto-initialises on DOMContentLoaded.
 | `stm-action="cart.checkout"` | Invokes the action reference resolved from state. On a `<form>`, collects form data; on a button, fires on click. |
 | `stm-data-key="value"` | Provides data to the enclosing `stm-action`. |
 
+`stm-markdown` targets the CommonMark 0.31.2 specification: paragraphs break
+only on blank lines (a single newline is a soft break, not a new paragraph),
+emphasis may span a line wrap, and a wrapped line stays inside its list item.
+
+```html
+<div stm-markdown="post.body"></div>
+```
+
+With `post.body` set to:
+
+```markdown
+**bold text
+that wraps** across lines
+
+- a list item
+  whose text continues on the next line
+- another item
+```
+
+the element renders as one `<strong>` spanning both lines, a single paragraph,
+and a two-item list whose first item contains both of its lines.
+
 ### Guards
 
 ```html
@@ -441,6 +463,23 @@ statum.afterEvaluate(function() {
 });
 ```
 
+### Running the markdown tests
+
+The `stm-markdown` renderer (`js/statum.js`) has a JavaScript test suite that
+does not require Node.js — it runs under
+[QuickJS](https://bellard.org/quickjs/), whose sources are vendored in
+`tools/qjs/` and compiled on demand by `gcc`:
+
+```sh
+tests/markdown/run.sh          # regressions + CommonMark spec suite
+tests/markdown/run.sh -v       # also list every failing spec example
+```
+
+The run reports the conformance rate against the official CommonMark 0.31.2
+example suite (excluding the raw-HTML sections, which Statum escapes by
+design) and fails when it drops below the 95% target. See
+`tests/markdown/README.md` for details.
+
 ---
 
 ## Vala API Reference

+ 33 - 1
model.md

@@ -318,7 +318,39 @@ The attribute `stm-html` binds the element's `innerHTML` to the specified state
 
 ## Markdown binding
 
-The attribute `stm-markdown` renders a state value as **safe markdown**: all HTML in the source is escaped first, then markdown formatting (headings, bold, italic, inline code, fenced code blocks, links, lists, blockquotes, horizontal rules, paragraphs) is applied. This is safe for untrusted user input — no raw HTML can be injected.
+The attribute `stm-markdown` renders a state value as **CommonMark** (the unambiguous Markdown specification, version 0.31.2), converted to safe HTML. For example `<div stm-markdown="post.body">`.
+
+The renderer is a two-pass CommonMark engine: block structure is computed over the whole document first, then inline content is parsed inside the leaf blocks. That is what makes the constructs below work across line wraps — the thing naive line-by-line renderers get wrong.
+
+### Block structure
+
+- **Paragraphs** break only on a *blank* line. A single newline inside a paragraph is a *soft line break*: the text simply continues (rendered as a newline, not a new paragraph and not a `<br>`). Source that was hard-wrapped at 80 columns therefore renders as one flowing paragraph.
+- **Emphasis and strong emphasis** (`*`/`_`, `**`/`__`, `***`, nesting) are resolved with the CommonMark delimiter-run algorithm, so a span may cross a line wrap — `**bold text` + newline + `that wraps**` is a single `<strong>` — and intraword emphasis (`foo*bar*baz`) follows the spec.
+- **Headings**: ATX (`# Heading`, with closing `#`s trimmed) and setext (a paragraph followed by a line of `=` or `-`).
+- **Lists**: ordered and bullet, with the spec's tight/loose rendering (a blank line between items makes a loose list whose items are wrapped in `<p>`), nesting, items that continue over multiple lines (a wrapped line stays inside its `<li>`), and item-indent code blocks.
+- **Block quotes** (`>`), including lazy continuation: a plain paragraph line following an open quote continues it.
+- **Code blocks**: indented (4 spaces) and fenced with `` ``` `` or `~~~`, optionally with an info string — `` ```html `` names the block, emitted as `<code class="language-html">`.
+- **Thematic breaks**: `---`, `***` or `___` render as `<hr>`.
+- **Link reference definitions** (`[label]: /url "title"`) are consumed and used by full (`[label][label]`), collapsed (`[label][]`) and shortcut (`[label]`) links anywhere in the document.
+
+### Inline content
+
+- Code spans (`` `code` ``), with backtick-count matching and leading/trailing space stripping.
+- Links and images — inline `[text](/url "title")` and the reference forms above — with titles.
+- Autolinks: `<https://example.com>` and `<mail@example.com>` (bare emails become `mailto:` links).
+- **Hard line breaks**: two or more trailing spaces, or a trailing backslash. A soft break (single newline) is deliberately *not* turned into a `<br>`.
+- Backslash escapes for ASCII punctuation (`\*` is a literal `*`).
+- Entity references: named (`&copy;`, `&frac12;` — the WHATWG entity table), decimal (`&#169;`) and hex (`&#xA9;`).
+
+### Security posture
+
+The markdown source is untrusted text and is rendered with an **escape-first** posture:
+
+- **Raw HTML never becomes markup.** Inline HTML (for example `<script>alert(1)</script>`) and HTML-block constructs render as escaped *text*. This is the one intentional divergence from the CommonMark reference output, which passes raw HTML through.
+- **Link and image destinations are scheme-filtered**: relative URLs and the `http`, `https`, `mailto` and `ftp` schemes pass through; any other scheme (`javascript:`, `data:`, `vbscript:` …) keeps its link text but the `href`/`src` is replaced with `#`.
+- Tables, strikethrough (`~~x~~`) and task lists are not supported — they are extensions, not CommonMark.
+
+Conformance is verified against the official CommonMark 0.31.2 example suite by `tests/markdown/run.sh` (see *Getting started* for how to run it).
 
 ## Header demotion