@mapled/cli 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -97,7 +97,7 @@ npx @mapled/cli scan
97
97
  npx @mapled/cli scan --write
98
98
  ```
99
99
 
100
- The scanner reads the site's source and writes down what it reads from Mapled: the pages (from the framework's file layout — Next.js App and Pages Router, Astro, SvelteKit, Nuxt, Remix) and one binding per place a collection or field is used.
100
+ The scanner reads the site's source and writes down what it reads from Mapled: the pages (from the framework's file layout — Next.js App and Pages Router, Astro, SvelteKit, Nuxt, Remix — or a plain HTML site's `.html` files) and one binding per place a collection or field is used.
101
101
 
102
102
  ```text
103
103
  Scanned 42 files with TypeScript — 3 pages, 14 bindings.
@@ -113,6 +113,21 @@ No mapled/manifest.json yet — run `mapled scan --write` to create it, then `ma
113
113
 
114
114
  It follows `getRecords` / `getRecord` / `getRecordBySlug` / `getSingle` calls through variables, destructuring, `.map()` callbacks and into the components a record is passed to as a prop, and through helpers in `lib/` that return a read. The parser is your repository's own TypeScript; without it the scanner still finds the collections, but not the fields. Source code never leaves your machine — only the manifest does, when you push it.
115
115
 
116
+ **A plain HTML site** (`framework` is `plain-html` in `mapled.json` — a folder with an `index.html` and no framework in `package.json`) has no reads in code: the scanner reads the `data-mapled-*` attributes of its `.html` files the way the [`@mapled/vanilla`](https://www.npmjs.com/package/@mapled/vanilla) script does, in the tree a browser builds from the file (the CLI carries the standard HTML parser, parse5, inside — nothing to install). Each file is a page where a static host serves it (`index.html` → `/`, `blog/index.html` → `/blog/`, `post.html` → `/post.html`, `public/` from the root); a record read by `data-mapled-slug-param` has its own address with that parameter in the query, so **Preview** in the record's editor opens the right page — and the scan says when Preview can't open an address. The target of a binding is what the attribute renders — `data-mapled-format="markdown"` rich text, an `<img>` an image, `data-mapled-bind-href` a link — and each different way a field is read is a binding of its own, so a field bound where it can't render shows up as a mismatch even when another element reads it well. With the schema at hand it also names the paths nothing in Mapled answers to: on the page, a misspelled field and an empty one look the same.
117
+
118
+ ```text
119
+ Scanned 2 HTML files — 2 pages, 11 bindings.
120
+
121
+ / index.html
122
+ home headline, intro, tagline
123
+ posts list • date, slug, title
124
+ /post.html?slug=[slug] post.html
125
+ home headline
126
+ posts by slug • body, title
127
+
128
+ ⚠ This path names nothing in the schema: home.tagline (index.html). A misspelled field empties its place on the page, a misspelled single leaves the page's own HTML.
129
+ ```
130
+
116
131
  `--write` creates or updates `mapled/manifest.json`. Bindings the scan can't see (written by your AI agent, or by hand) are kept and listed; `--prune` drops them. Commit the file.
117
132
 
118
133
  ## Validate and push the manifest
package/dist/commands.js CHANGED
@@ -12,6 +12,7 @@ import { formatChecks, GLYPH, paint, summarize, useColor } from "./output.js";
12
12
  import { diffSchema, formatChanges, pinSchema, readPin, SCHEMA_FILE, summarizeChanges, writePin } from "./pin.js";
13
13
  import { checkMapledMd, MD_FILE, mergeMd } from "./md.js";
14
14
  import { loadTypeScript, mergeManifest, scanRepository } from "./scan.js";
15
+ import { scanHtml } from "./scanHtml.js";
15
16
  import { generateTypes } from "./types.js";
16
17
  /** The OAuth client id for this API — registered once per machine, and
17
18
  again when the server no longer knows it. */
@@ -490,8 +491,8 @@ export async function scan(ctx, flags) {
490
491
  throw new CliError(`The scan needs the schema to name the fields — sign in with \`mapled auth login\` or run \`mapled schema pull\` first.`);
491
492
  }
492
493
  const framework = found.config.framework ?? (await dr.detectFramework(found.dir)) ?? null;
493
- const ts = loadTypeScript(found.dir);
494
- const result = await scanRepository(found.dir, { framework, schema, ts });
494
+ // a plain HTML site declares its reads in attributes, not in code
495
+ const result = framework === "plain-html" ? await scanHtml(found.dir, { schema }) : await scanRepository(found.dir, { framework, schema, ts: loadTypeScript(found.dir) });
495
496
  const existing = await loadManifest(found.dir);
496
497
  const merged = mergeManifest(existing?.manifest ?? null, result, { prune: Boolean(flags.prune) });
497
498
  if (flags.json) {
@@ -500,7 +501,7 @@ export async function scan(ctx, flags) {
500
501
  await writeManifestFile(found.dir, merged.manifest);
501
502
  return;
502
503
  }
503
- ctx.out(`Scanned ${plural(result.files, "file")} with ${result.parser === "typescript" ? "TypeScript" : "the tokenizer"} — ${plural(result.pages.length, "page")}, ${plural(result.bindings.length, "binding")}.`);
504
+ ctx.out(`${result.parser === "html" ? `Scanned ${plural(result.files, "HTML file")}` : `Scanned ${plural(result.files, "file")} with ${result.parser === "typescript" ? "TypeScript" : "the tokenizer"}`} — ${plural(result.pages.length, "page")}, ${plural(result.bindings.length, "binding")}.`);
504
505
  if (result.bindings.length > 0) {
505
506
  ctx.out("");
506
507
  for (const line of describeScan(result, schema))
@@ -0,0 +1,66 @@
1
+ import { type Token, type TreeAdapter, type TreeAdapterTypeMap } from "parse5";
2
+ /** The HTML parser of `mapled scan` for a plain HTML site: parse5, the
3
+ WHATWG parsing algorithm — the tree a browser builds, and the one
4
+ jsdom builds for the runtime's tests. Tag soup decides which scope a
5
+ binding lands in (a <p> a <div> closes, a table's foster parent, a
6
+ template an end tag can't leave), so the scan can't read a tree of its
7
+ own. build.mjs bundles parse5 (pinned) into dist/html5.js: the CLI
8
+ still installs without dependencies.
9
+
10
+ What a page can make the parser do is held linear in its size — a
11
+ page is the site's to write, and the scan reads up to 2 MB of it:
12
+ - open elements: at most MAX_OPEN (Guarded.onItemPush). Every start tag
13
+ looks down them for a scope; 100,000 unclosed <div>s cost minutes.
14
+ - a tag's attributes: a set of names (Guarded, _leaveAttrName). parse5
15
+ compared each new one with every earlier one — 64,000 on one <div>: 4 s.
16
+ - the tree: children in linked lists (`linked` below), every operation
17
+ O(1). parse5's own tree keeps arrays, and foster parenting (text
18
+ before a <table>: indexOf), the adoption agency (a misnested </b>:
19
+ splice from the front) and repeated <html>/<body> tags (every
20
+ attribute so far) walk them — 160,000 "a<br>" in a table: 11 s.
21
+ The worst page within the cap is the depth times its start tags. */
22
+ /** How deep elements may be open at once — Chrome's own limit of the tree it builds. */
23
+ export declare const MAX_OPEN = 512;
24
+ export declare class TooDeep extends Error {
25
+ }
26
+ export type Html5Attr = Token.Attribute;
27
+ /** A node of the tree: an element (tagName), text, a comment, a doctype,
28
+ the document or a template's content — children linked both ways. Its
29
+ fields are named as parse5's own tree names them; `childNodes` is made
30
+ when asked for (the parser never asks). */
31
+ export declare class Html5Node {
32
+ readonly nodeName: string;
33
+ tagName?: string;
34
+ namespaceURI?: string;
35
+ attrs?: Html5Attr[];
36
+ /** a <template>'s content: what the page's author wrote inside it */
37
+ content?: Html5Node;
38
+ parentNode: Html5Node | null;
39
+ firstChild: Html5Node | null;
40
+ lastChild: Html5Node | null;
41
+ previousSibling: Html5Node | null;
42
+ nextSibling: Html5Node | null;
43
+ /** a text's */
44
+ value?: string;
45
+ /** a comment's */
46
+ data?: string;
47
+ /** a doctype's */
48
+ name?: string;
49
+ publicId?: string;
50
+ systemId?: string;
51
+ /** the document's */
52
+ mode?: string;
53
+ /** the names an <html> or <body> has, as repeated tags add to them */
54
+ names?: Set<string>;
55
+ location?: Token.ElementLocation | null;
56
+ constructor(nodeName: string, extra?: Partial<Html5Node>);
57
+ /** none on a text, a comment or a doctype, as in parse5's tree */
58
+ get childNodes(): Html5Node[] | undefined;
59
+ }
60
+ type Map_ = TreeAdapterTypeMap<Html5Node, Html5Node, Html5Node, Html5Node, Html5Node, Html5Node, Html5Node, Html5Node, Html5Node, Html5Node>;
61
+ /** parse5's default tree adapter, method for method, on linked children. */
62
+ export declare const linked: TreeAdapter<Map_>;
63
+ /** The document, with scripting on — as the runtime sees it (a <noscript>
64
+ is text). Throws TooDeep past MAX_OPEN open elements. */
65
+ export declare function parseDocument(text: string): Html5Node;
66
+ export {};