comark-file-tree 0.7.6 → 0.8.1

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
@@ -33,17 +33,17 @@ README.md
33
33
 
34
34
  Indent each level by two spaces, or paste what `tree` prints. A trailing `/` makes a folder; `+`, `-`, and `~` mark an entry added, removed, or changed; ` # ` starts a note; and `...` stands for entries left out. Pass `iconResolver` from comark-icon's `createIcons()` to draw each file's icon; without it, entries get plain glyphs and nothing is fetched.
35
35
 
36
- The plugin adds every problem to `meta.issues`, so a host can reject a document before publishing it: a Markdown list instead of a fence (a list turns `__init__.py` bold and drops `[id].vue`), content beside the fence, and an empty block. Each issue names its block and says how to fix it.
36
+ The plugin adds every problem to `meta.issues`, so a host can reject a document before publishing it, such as a Markdown list instead of a fence (a list turns `__init__.py` bold and drops `[id].vue`), content beside the fence, and an empty block.
37
37
 
38
38
  ## Renderers
39
39
 
40
40
  | Renderer | Import |
41
41
  | --- | --- |
42
- | `@comark/html` | `comark-file-tree/html`: the styles are inlined once per document |
42
+ | `@comark/html` | `comark-file-tree/html`: the styles are inlined |
43
43
  | `@comark/vue` | `comark-file-tree/vue` |
44
44
  | `@comark/react` | `comark-file-tree/react` |
45
45
  | `@comark/svelte` | `comark-file-tree/svelte` |
46
46
 
47
47
  Each entry exports the parse plugin as `default` and the renderer's `components`. The Vue, React, and Svelte components add the styles when they mount; import `comark-file-tree/style.css` with your styles so a server-rendered page doesn't shift.
48
48
 
49
- The styles follow shadcn/ui: they read its CSS variables, such as `--accent` and `--border`, when the page has them, and use its neutral theme otherwise. Set the shared `--comark-*` tokens to theme every plugin at once; the rules sit in a `comark` CSS layer, so any plain rule of your own wins, except the spacing that draws the tree: the margins, padding, and bullets of its lists, `details`, and `summary` are `!important`, because a page's own list styles are almost always unlayered and would push entries off their guide lines. Override those with `!important` too.
49
+ The styles follow shadcn/ui: they read its CSS variables, such as `--accent` and `--border`, when the page has them, and use its neutral theme otherwise. Set the shared `--comark-*` tokens to theme every plugin at once; the rules sit in a `comark` CSS layer, so any plain rule of your own wins, except the spacing that draws the tree: the margins, padding, and bullets of its lists, `details`, and `summary` are `!important`. Override those with `!important` too.
@@ -43,20 +43,97 @@ const tokens = [
43
43
  "#737373"
44
44
  ]
45
45
  ];
46
- const resolve = (scheme) => tokens.map((token) => `--_comark-${token[0]}:var(--comark-${token[0]},var(--${token[1]},var(--${token[2]},${token[scheme]})))`).join(";");
46
+ const groups = {
47
+ status: Object.values({
48
+ success: [
49
+ "success",
50
+ "ui-success",
51
+ "success",
52
+ "#259f56",
53
+ "#59d38c"
54
+ ],
55
+ danger: [
56
+ "danger",
57
+ "ui-error",
58
+ "destructive",
59
+ "#ee3533",
60
+ "#fd736d"
61
+ ],
62
+ warning: [
63
+ "warning",
64
+ "ui-warning",
65
+ "",
66
+ "#d97706",
67
+ "#fbbf24"
68
+ ],
69
+ info: [
70
+ "info",
71
+ "ui-info",
72
+ "",
73
+ "#2563eb",
74
+ "#60a5fa"
75
+ ]
76
+ }),
77
+ primary: [
78
+ [
79
+ "primary",
80
+ "ui-primary",
81
+ "primary",
82
+ "var(--_comark-foreground)",
83
+ "var(--_comark-foreground)"
84
+ ],
85
+ [
86
+ "primary-foreground",
87
+ "ui-text-inverted",
88
+ "primary-foreground",
89
+ "var(--_comark-background)",
90
+ "var(--_comark-background)"
91
+ ],
92
+ [
93
+ "secondary",
94
+ "ui-secondary",
95
+ "",
96
+ "var(--_comark-muted)",
97
+ "var(--_comark-muted)"
98
+ ]
99
+ ],
100
+ shadow: [[
101
+ "shadow",
102
+ "",
103
+ "",
104
+ "0 4px 12px rgb(0 0 0/.12)",
105
+ "0 4px 12px rgb(0 0 0/.5)"
106
+ ]]
107
+ };
108
+ const chain = (token, scheme) => {
109
+ const shadcn = token[2] ? `var(--${token[2]},${token[scheme]})` : token[scheme];
110
+ const nuxt = token[1] ? `var(--${token[1]},${shadcn})` : shadcn;
111
+ return `var(--comark-${token[0]},${nuxt})`;
112
+ };
113
+ const resolve = (list, scheme) => list.map((token) => `--_comark-${token[0]}:${chain(token, scheme)}`).join(";");
47
114
  /**
48
115
  * Resolves the `--comark-*` tokens on `selector`, falling back to Nuxt UI's variables, then shadcn/ui's, then shadcn's
49
116
  * neutral theme. Nuxt UI's corners are twice its `--ui-radius`, so the radius halves shadcn's to double it back.
50
117
  * They resolve on the component rather than `:root`, so a `.dark` class anywhere above it switches the theme, and a
51
118
  * `--comark-*` set on any ancestor still inherits down. `:where()` keeps them at zero specificity.
119
+ *
120
+ * Every block gets the core tokens. Name `"status"` for `--comark-success`, `-danger`, `-warning`, and `-info`,
121
+ * `"primary"` for `--comark-primary`, `-primary-foreground`, and `-secondary`, and `"shadow"` for `--comark-shadow`, so
122
+ * a block that never draws them doesn't carry them.
52
123
  */
53
- function theme(selector) {
124
+ function theme(selector, ...extras) {
54
125
  const where = `:where(${selector})`;
55
- return `${where}{${resolve(3)};--_comark-radius:var(--comark-radius,calc(var(--ui-radius,calc(var(--radius,.625rem)/2))*2));--_comark-font:var(--comark-font,var(--font-sans,ui-sans-serif,system-ui,sans-serif))}@media (prefers-color-scheme:dark){${where}{${resolve(4)}}}`;
126
+ const added = extras.flatMap((name) => groups[name]);
127
+ const list = [...tokens, ...added];
128
+ const schemed = added.filter((token) => token[3] !== token[4]);
129
+ const classes = schemed.length ? `:where(.light,.light *)${where}{${resolve(schemed, 3)}}:where(.dark,.dark *)${where}{${resolve(schemed, 4)}}` : "";
130
+ return `${where}{${resolve(list, 3)};--_comark-radius:var(--comark-radius,calc(var(--ui-radius,calc(var(--radius,.625rem)/2))*2));--_comark-font:var(--comark-font,var(--font-sans,ui-sans-serif,system-ui,sans-serif))}@media (prefers-color-scheme:dark){${where}{${resolve(list, 4)}}}` + classes;
56
131
  }
57
132
  //#endregion
58
133
  //#region ../../shared/disclosure.ts
59
- const disclosureMotion = (selector) => `@supports (interpolate-size:allow-keywords) and selector(::details-content){@media screen and (prefers-reduced-motion:no-preference){${selector}{interpolate-size:allow-keywords}${selector}::details-content{display:flow-root;block-size:0;overflow-y:clip;transition:block-size .25s cubic-bezier(.23,1,.32,1),content-visibility .25s allow-discrete}${selector}[open]::details-content{block-size:auto}}}`;
134
+ const opening = ".25s cubic-bezier(.23,1,.32,1)";
135
+ const closing = ".18s cubic-bezier(.23,1,.32,1)";
136
+ const disclosureMotion = (selector, rules = "") => `@supports (interpolate-size:allow-keywords) and selector(::details-content){@media screen and (prefers-reduced-motion:no-preference){${selector}{interpolate-size:allow-keywords}${selector}::details-content{display:flow-root;block-size:0;opacity:0;overflow-y:clip;transition:block-size ${closing},opacity .12s ease-in,content-visibility .18s allow-discrete}${selector}[open]::details-content{block-size:auto;opacity:1;transition:block-size ${opening},opacity ${opening},content-visibility .25s allow-discrete}${rules}}}`;
60
137
  //#endregion
61
138
  //#region src/styles.ts
62
139
  /**
@@ -66,9 +143,8 @@ const disclosureMotion = (selector) => `@supports (interpolate-size:allow-keywor
66
143
  */
67
144
  const css = `@layer comark{${[
68
145
  disclosureMotion(".cft details"),
69
- theme(".cft"),
70
- ":where(.cft){--cft-added:#16a34a;--cft-removed:var(--destructive,#dc2626);--cft-changed:#d97706}",
71
- "@media (prefers-color-scheme:dark){:where(.cft){--cft-added:#4ade80;--cft-removed:var(--destructive,#f87171);--cft-changed:#fbbf24}}",
146
+ theme(".cft", "status"),
147
+ ":where(.cft){--cft-added:var(--_comark-success);--cft-removed:var(--_comark-danger);--cft-changed:var(--_comark-warning)}",
72
148
  ".cft{margin:1.5rem 0;padding:.75rem;overflow-x:auto;border:1px solid var(--_comark-border);border-radius:var(--_comark-radius);background:var(--_comark-background);color:var(--_comark-foreground);font:400 .8125rem/1.25rem ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}",
73
149
  ".cft ul{margin:0!important;padding:0!important;list-style:none!important}",
74
150
  ".cft ul ul{margin-inline-start:calc(.5rem - .5px)!important;padding-inline-start:.75rem!important;border-inline-start:1px solid var(--_comark-border)}",
@@ -90,15 +166,23 @@ const css = `@layer comark{${[
90
166
  ".cft [data-type='more']{padding:0 .25rem;color:var(--_comark-muted)}"
91
167
  ].join("")}}`;
92
168
  //#endregion
93
- //#region src/client.ts
94
- /** Adds the file tree styles to the page once. The Vue, React, and Svelte components call it when they mount. */
95
- function addStyles() {
96
- if (document.querySelector("style[data-comark-file-tree]")) return;
169
+ //#region ../../shared/styles.ts
170
+ /**
171
+ * Adds a package's styles to the page once, unless the page already carries them as a `style` or a `link` with
172
+ * `data-comark-<name>`, which is how the HTML renderer inlines them and how an app that imports `style.css` marks them.
173
+ */
174
+ function addStyles$1(name, css) {
175
+ const attribute = `data-comark-${name}`;
176
+ if (document.querySelector(`style[${attribute}],link[${attribute}]`)) return;
97
177
  const style = document.createElement("style");
98
- style.dataset.comarkFileTree = "";
178
+ style.setAttribute(attribute, "");
99
179
  style.textContent = css;
100
180
  document.head.append(style);
101
181
  }
182
+ //#endregion
183
+ //#region src/client.ts
184
+ /** Adds the file tree styles to the page once. The Vue, React, and Svelte components call it when they mount. */
185
+ const addStyles = () => addStyles$1("file-tree", css);
102
186
  /**
103
187
  * Lucide's folder and file (ISC), each as one path, for entries without an icon. Every renderer draws them in the
104
188
  * same `<svg>`, sized for a page without the styles.
package/dist/client.d.mts CHANGED
@@ -8,7 +8,7 @@ export declare const css: string;
8
8
  //#endregion
9
9
  //#region src/client.d.ts
10
10
  /** Adds the file tree styles to the page once. The Vue, React, and Svelte components call it when they mount. */
11
- export declare function addStyles(): void;
11
+ export declare const addStyles: () => void;
12
12
  /**
13
13
  * Lucide's folder and file (ISC), each as one path, for entries without an icon. Every renderer draws them in the
14
14
  * same `<svg>`, sized for a page without the styles.
package/dist/client.mjs CHANGED
@@ -1 +1 @@
1
- export { t as addStyles, i as css, n as glyphs, r as marks } from "./client-Djg5meKD.mjs";
1
+ export { t as addStyles, i as css, n as glyphs, r as marks } from "./client-CmwF7cCV.mjs";
package/dist/html.mjs CHANGED
@@ -1,10 +1,28 @@
1
- import src_default from "./index.mjs";
2
- import { i as css, n as glyphs, r as marks } from "./client-Djg5meKD.mjs";
1
+ import { t as src_default } from "./src-PLku93wQ.mjs";
2
+ import { i as css, n as glyphs, r as marks } from "./client-CmwF7cCV.mjs";
3
3
  import { escapeHtml } from "comark/utils";
4
4
  import { iconHtml, iconStyles } from "comark-icon/html";
5
+ //#region ../../shared/text.ts
6
+ /**
7
+ * A prop value as text. Comark writes strings, numbers, and booleans, so an object, array, function, or missing value
8
+ * is no text. The `typeof` check, unlike `instanceof`, can't throw on a revoked proxy.
9
+ */
10
+ function scalar(value) {
11
+ switch (typeof value) {
12
+ case "string": return value;
13
+ case "number":
14
+ case "boolean":
15
+ case "bigint":
16
+ case "symbol": return String(value);
17
+ default: return;
18
+ }
19
+ }
20
+ /** A prop value as text exactly as written, or empty when it has none. */
21
+ const rawText = (value) => scalar(value) ?? "";
22
+ //#endregion
5
23
  //#region src/html.ts
6
24
  const styled = /* @__PURE__ */ new WeakSet();
7
- const text = (value) => value === void 0 ? "" : escapeHtml(String(value));
25
+ const text = (value) => escapeHtml(rawText(value));
8
26
  const glyph = (d) => `<svg class="cft-glyph" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="${d}"></path></svg>`;
9
27
  const hasIcon = (node) => Array.isArray(node) && (Boolean(node[1]?.icon) || node.slice(2).some(hasIcon));
10
28
  const fileTree = async (node, state) => {
package/dist/index.mjs CHANGED
@@ -1,209 +1 @@
1
- import { defineComarkPlugin } from "comark";
2
- import { textContent } from "comark/utils";
3
- //#region src/index.ts
4
- const isElement = (node) => Array.isArray(node) && node[0] !== null;
5
- const isContent = (node) => Array.isArray(node) ? node[0] !== null : node.trim() !== "";
6
- const childrenOf = (node) => node.slice(2);
7
- const clip = (value) => {
8
- const words = value.trim().replace(/\s+/g, " ");
9
- return words.length > 40 ? `${words.slice(0, 39)}…` : words;
10
- };
11
- const ICONS = {
12
- folder: "vscode-icons:default-folder-opened",
13
- file: "vscode-icons:default-file",
14
- ".ts": "vscode-icons:file-type-typescript-official",
15
- ".mts": "vscode-icons:file-type-typescript-official",
16
- ".cts": "vscode-icons:file-type-typescript-official",
17
- ".tsx": "vscode-icons:file-type-reactts",
18
- ".js": "vscode-icons:file-type-js-official",
19
- ".mjs": "vscode-icons:file-type-js-official",
20
- ".cjs": "vscode-icons:file-type-js-official",
21
- ".jsx": "vscode-icons:file-type-reactjs",
22
- ".json": "vscode-icons:file-type-json",
23
- ".md": "vscode-icons:file-type-markdown",
24
- ".mdx": "vscode-icons:file-type-mdx",
25
- ".vue": "vscode-icons:file-type-vue",
26
- ".svelte": "vscode-icons:file-type-svelte",
27
- ".astro": "vscode-icons:file-type-astro",
28
- ".css": "vscode-icons:file-type-css",
29
- ".scss": "vscode-icons:file-type-scss",
30
- ".html": "vscode-icons:file-type-html",
31
- ".yaml": "vscode-icons:file-type-yaml",
32
- ".yml": "vscode-icons:file-type-yaml",
33
- ".toml": "vscode-icons:file-type-toml",
34
- ".py": "vscode-icons:file-type-python",
35
- ".rs": "vscode-icons:file-type-rust",
36
- ".go": "vscode-icons:file-type-go",
37
- ".sh": "vscode-icons:file-type-shell",
38
- ".svg": "vscode-icons:file-type-svg",
39
- ".png": "vscode-icons:file-type-image",
40
- ".jpg": "vscode-icons:file-type-image",
41
- ".jpeg": "vscode-icons:file-type-image",
42
- ".gif": "vscode-icons:file-type-image",
43
- ".webp": "vscode-icons:file-type-image",
44
- ".txt": "vscode-icons:file-type-text",
45
- ".env": "vscode-icons:file-type-dotenv",
46
- ".gitignore": "vscode-icons:file-type-git",
47
- "package.json": "vscode-icons:file-type-npm",
48
- "tsconfig.json": "vscode-icons:file-type-tsconfig",
49
- "pnpm-lock.yaml": "vscode-icons:file-type-pnpm",
50
- "pnpm-workspace.yaml": "vscode-icons:file-type-pnpm",
51
- dockerfile: "vscode-icons:file-type-docker",
52
- license: "vscode-icons:file-type-license"
53
- };
54
- const STATES = /* @__PURE__ */ new Map([
55
- ["+", "added"],
56
- ["-", "removed"],
57
- ["~", "changed"]
58
- ]);
59
- const BRANCHES = /^(?:[│|][\s ]{2,3}|[\s ])*(?:[├└|`+\\](?:──|--)[─-]*[\s ]?)?/u;
60
- const NOTE = /\s+#\s+(.*)$/;
61
- const SUMMARY = /^\d+ director(?:y|ies)(?:, \d+ files?)?$/;
62
- function entry(source) {
63
- let rest = source;
64
- const note = NOTE.exec(rest);
65
- if (note) rest = rest.slice(0, note.index);
66
- rest = rest.trim();
67
- const marker = /^([+~-])\s+/.exec(rest);
68
- if (marker) rest = rest.slice(marker[0].length);
69
- if (!rest) return void 0;
70
- const attrs = {};
71
- if (rest === "..." || rest === "…") attrs.type = "more";
72
- else if (rest.endsWith("/")) {
73
- attrs.type = "folder";
74
- attrs.name = rest.replace(/\/+$/, "");
75
- } else {
76
- attrs.type = "file";
77
- attrs.name = rest;
78
- }
79
- const state = STATES.get(marker?.[1] ?? "");
80
- if (state) attrs.state = state;
81
- if (note?.[1]?.trim()) attrs.note = note[1].trim();
82
- return ["file-tree-item", attrs];
83
- }
84
- function readTree(source) {
85
- const roots = [];
86
- const open = [];
87
- for (const raw of source.split("\n")) {
88
- const line = raw.replace(/\s+$/, "");
89
- const column = BRANCHES.exec(line)?.[0].length ?? 0;
90
- if (SUMMARY.test(line.slice(column))) continue;
91
- const item = entry(line.slice(column));
92
- if (!item) continue;
93
- while (open.length && (open.at(-1)?.column ?? 0) >= column) open.pop();
94
- const parent = open.at(-1)?.item;
95
- if (parent) {
96
- if (parent[1].type === "file") parent[1].type = "folder";
97
- parent.push(item);
98
- } else roots.push(item);
99
- if (item[1].type !== "more") open.push({
100
- column,
101
- item
102
- });
103
- }
104
- const [only] = roots;
105
- if (roots.length === 1 && only?.[1].name === ".") return childrenOf(only).filter(isElement);
106
- return roots;
107
- }
108
- function iconKey(item, icons) {
109
- const name = (item[1].name ?? "").toLowerCase();
110
- if (item[1].type === "folder") return icons[`${name}/`] ?? icons.folder;
111
- const dot = name.lastIndexOf(".");
112
- return icons[name] ?? (dot >= 0 ? icons[name.slice(dot)] : void 0) ?? icons.file;
113
- }
114
- function* items(nodes) {
115
- for (const node of nodes) {
116
- if (!isElement(node)) continue;
117
- if (node[0] === "file-tree-item") yield node;
118
- yield* items(childrenOf(node));
119
- }
120
- }
121
- const KINDS = /* @__PURE__ */ new Map([
122
- ["p", "paragraph"],
123
- ["ul", "list"],
124
- ["ol", "list"],
125
- ["blockquote", "quote"],
126
- ["table", "table"]
127
- ]);
128
- function describe(node) {
129
- if (!Array.isArray(node)) return `the text "${clip(node)}"`;
130
- if (!isElement(node)) return "a comment";
131
- const kind = KINDS.get(node[0]) ?? `\`${node[0]}\``;
132
- const words = clip(textContent(node));
133
- return `the ${kind}${words ? ` "${words}"` : ""}`;
134
- }
135
- function fileTree(node, name, issues) {
136
- for (const key of Object.keys(node[1])) {
137
- const prop = key.replace(/^:/, "");
138
- if (prop !== "id" && prop !== "class") issues.push({
139
- rule: "comark/file-tree/unknown-prop",
140
- message: `${name}: \`${prop}\` isn't a prop of \`::file-tree\`, which takes none, so it's ignored. Remove it.`
141
- });
142
- }
143
- const content = childrenOf(node).filter(isContent);
144
- const [block] = content.filter((child) => isElement(child) && child[0] === "pre");
145
- if (!isElement(block)) {
146
- const list = content.find((child) => isElement(child) && /^[ou]l$/.test(child[0]));
147
- issues.push(list ? {
148
- rule: "comark/file-tree/list-instead-of-fence",
149
- message: `${name} holds a Markdown list, which turns names like \`__init__.py\` bold and drops \`[id].vue\`. Write the tree in a fenced block.`
150
- } : {
151
- rule: "comark/file-tree/empty-tree",
152
- message: `${name} has no tree. Write the tree in a fenced block inside it, one entry per line.`
153
- });
154
- return content;
155
- }
156
- for (const child of content) if (child !== block) issues.push({
157
- rule: "comark/file-tree/content-dropped",
158
- message: `${name}: only one fenced block goes inside, so ${describe(child)} doesn't render. Move it out of the block.`
159
- });
160
- const tree = readTree(textContent(block));
161
- if (!tree.length) issues.push({
162
- rule: "comark/file-tree/empty-tree",
163
- message: `${name}'s fenced block is empty. Write one entry per line.`
164
- });
165
- node.splice(1, node.length - 1, {}, ...tree);
166
- return [node];
167
- }
168
- /**
169
- * Turns `::file-tree` around a fenced block, one entry per line indented by two spaces or pasted from `tree`, into a
170
- * tree of folders and files. A trailing `/` makes a folder, `+`, `-`, and `~` mark an entry added, removed, or
171
- * changed, `# note` ends a line with a note, and `...` stands for entries left out. Problems are added to `meta.issues`.
172
- */
173
- var src_default = defineComarkPlugin((options = {}) => {
174
- const icons = Object.fromEntries(Object.entries({
175
- ...ICONS,
176
- ...options.icons
177
- }).map(([key, icon]) => [key.toLowerCase(), icon]));
178
- return {
179
- name: "comark-file-tree",
180
- async post(state) {
181
- const { tree } = state;
182
- const issues = [];
183
- let count = 0;
184
- const transform = (nodes) => nodes.flatMap((node) => {
185
- if (!isElement(node)) return [node];
186
- if (node[0] === "file-tree") return fileTree(node, `\`::file-tree\` ${++count}`, issues);
187
- node.splice(2, node.length - 2, ...transform(childrenOf(node)));
188
- return [node];
189
- });
190
- tree.nodes = transform(tree.nodes);
191
- if (options.iconResolver) {
192
- const all = [...items(tree.nodes)].filter((item) => item[1].type !== "more");
193
- const keys = all.map((item) => iconKey(item, icons));
194
- const resolved = await options.iconResolver.resolve(keys.filter((key) => Boolean(key)));
195
- for (const [at, item] of all.entries()) {
196
- const icon = resolved.get(keys[at] ?? "");
197
- if (icon && "src" in icon) item[1].icon = icon;
198
- }
199
- }
200
- tree.meta.issues = [...tree.meta.issues ?? [], ...issues.map((issue) => issue.message)];
201
- tree.meta.issueRules = {
202
- ...tree.meta.issueRules,
203
- ...Object.fromEntries(issues.map((issue) => [issue.message, issue.rule]))
204
- };
205
- }
206
- };
207
- });
208
- //#endregion
209
- export { src_default as default };
1
+ export { t as default } from "./src-PLku93wQ.mjs";
package/dist/react.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import src_default from "./index.mjs";
2
- import { n as glyphs, r as marks, t as addStyles } from "./client-Djg5meKD.mjs";
1
+ import { t as src_default } from "./src-PLku93wQ.mjs";
2
+ import { n as glyphs, r as marks, t as addStyles } from "./client-CmwF7cCV.mjs";
3
3
  import { Icon } from "comark-icon/react";
4
4
  import { createElement, useEffect } from "react";
5
5
  //#region src/react.ts
@@ -0,0 +1,211 @@
1
+ import { defineComarkPlugin } from "comark";
2
+ import { textContent } from "comark/utils";
3
+ //#region ../../shared/nodes.ts
4
+ const isElement = (node) => Array.isArray(node) && node[0] !== null;
5
+ const childrenOf = ([, , ...rest]) => rest;
6
+ //#endregion
7
+ //#region src/index.ts
8
+ const isContent = (node) => Array.isArray(node) ? node[0] !== null : node.trim() !== "";
9
+ const clip = (value) => {
10
+ const words = value.trim().replace(/\s+/g, " ");
11
+ return words.length > 40 ? `${words.slice(0, 39)}…` : words;
12
+ };
13
+ const ICONS = {
14
+ folder: "vscode-icons:default-folder-opened",
15
+ file: "vscode-icons:default-file",
16
+ ".ts": "vscode-icons:file-type-typescript-official",
17
+ ".mts": "vscode-icons:file-type-typescript-official",
18
+ ".cts": "vscode-icons:file-type-typescript-official",
19
+ ".tsx": "vscode-icons:file-type-reactts",
20
+ ".js": "vscode-icons:file-type-js-official",
21
+ ".mjs": "vscode-icons:file-type-js-official",
22
+ ".cjs": "vscode-icons:file-type-js-official",
23
+ ".jsx": "vscode-icons:file-type-reactjs",
24
+ ".json": "vscode-icons:file-type-json",
25
+ ".md": "vscode-icons:file-type-markdown",
26
+ ".mdx": "vscode-icons:file-type-mdx",
27
+ ".vue": "vscode-icons:file-type-vue",
28
+ ".svelte": "vscode-icons:file-type-svelte",
29
+ ".astro": "vscode-icons:file-type-astro",
30
+ ".css": "vscode-icons:file-type-css",
31
+ ".scss": "vscode-icons:file-type-scss",
32
+ ".html": "vscode-icons:file-type-html",
33
+ ".yaml": "vscode-icons:file-type-yaml",
34
+ ".yml": "vscode-icons:file-type-yaml",
35
+ ".toml": "vscode-icons:file-type-toml",
36
+ ".py": "vscode-icons:file-type-python",
37
+ ".rs": "vscode-icons:file-type-rust",
38
+ ".go": "vscode-icons:file-type-go",
39
+ ".sh": "vscode-icons:file-type-shell",
40
+ ".svg": "vscode-icons:file-type-svg",
41
+ ".png": "vscode-icons:file-type-image",
42
+ ".jpg": "vscode-icons:file-type-image",
43
+ ".jpeg": "vscode-icons:file-type-image",
44
+ ".gif": "vscode-icons:file-type-image",
45
+ ".webp": "vscode-icons:file-type-image",
46
+ ".txt": "vscode-icons:file-type-text",
47
+ ".env": "vscode-icons:file-type-dotenv",
48
+ ".gitignore": "vscode-icons:file-type-git",
49
+ "package.json": "vscode-icons:file-type-npm",
50
+ "tsconfig.json": "vscode-icons:file-type-tsconfig",
51
+ "pnpm-lock.yaml": "vscode-icons:file-type-pnpm",
52
+ "pnpm-workspace.yaml": "vscode-icons:file-type-pnpm",
53
+ dockerfile: "vscode-icons:file-type-docker",
54
+ license: "vscode-icons:file-type-license"
55
+ };
56
+ const STATES = /* @__PURE__ */ new Map([
57
+ ["+", "added"],
58
+ ["-", "removed"],
59
+ ["~", "changed"]
60
+ ]);
61
+ const BRANCHES = /^(?:[│|][\s ]{2,3}|[\s ])*(?:[├└|`+\\](?:──|--)[─-]*[\s ]?)?/u;
62
+ const NOTE = /\s+#\s+(.*)$/;
63
+ const SUMMARY = /^\d+ director(?:y|ies)(?:, \d+ files?)?$/;
64
+ function entry(source) {
65
+ let rest = source;
66
+ const note = NOTE.exec(rest);
67
+ if (note) rest = rest.slice(0, note.index);
68
+ rest = rest.trim();
69
+ const marker = /^([+~-])\s+/.exec(rest);
70
+ if (marker) rest = rest.slice(marker[0].length);
71
+ if (!rest) return void 0;
72
+ const attrs = {};
73
+ if (rest === "..." || rest === "…") attrs.type = "more";
74
+ else if (rest.endsWith("/")) {
75
+ attrs.type = "folder";
76
+ attrs.name = rest.replace(/\/+$/, "");
77
+ } else {
78
+ attrs.type = "file";
79
+ attrs.name = rest;
80
+ }
81
+ const state = STATES.get(marker?.[1] ?? "");
82
+ if (state) attrs.state = state;
83
+ if (note?.[1]?.trim()) attrs.note = note[1].trim();
84
+ return ["file-tree-item", attrs];
85
+ }
86
+ function readTree(source) {
87
+ const roots = [];
88
+ const open = [];
89
+ for (const raw of source.split("\n")) {
90
+ const line = raw.replace(/\s+$/, "");
91
+ const column = BRANCHES.exec(line)?.[0].length ?? 0;
92
+ if (SUMMARY.test(line.slice(column))) continue;
93
+ const item = entry(line.slice(column));
94
+ if (!item) continue;
95
+ while (open.length && (open.at(-1)?.column ?? 0) >= column) open.pop();
96
+ const parent = open.at(-1)?.item;
97
+ if (parent) {
98
+ if (parent[1].type === "file") parent[1].type = "folder";
99
+ parent.push(item);
100
+ } else roots.push(item);
101
+ if (item[1].type !== "more") open.push({
102
+ column,
103
+ item
104
+ });
105
+ }
106
+ const [only] = roots;
107
+ if (roots.length === 1 && only?.[1].name === ".") return childrenOf(only).filter(isElement);
108
+ return roots;
109
+ }
110
+ function iconKey(item, icons) {
111
+ const name = (item[1].name ?? "").toLowerCase();
112
+ if (item[1].type === "folder") return icons[`${name}/`] ?? icons.folder;
113
+ const dot = name.lastIndexOf(".");
114
+ return icons[name] ?? (dot >= 0 ? icons[name.slice(dot)] : void 0) ?? icons.file;
115
+ }
116
+ function* items(nodes) {
117
+ for (const node of nodes) {
118
+ if (!isElement(node)) continue;
119
+ if (node[0] === "file-tree-item") yield node;
120
+ yield* items(childrenOf(node));
121
+ }
122
+ }
123
+ const KINDS = /* @__PURE__ */ new Map([
124
+ ["p", "paragraph"],
125
+ ["ul", "list"],
126
+ ["ol", "list"],
127
+ ["blockquote", "quote"],
128
+ ["table", "table"]
129
+ ]);
130
+ function describe(node) {
131
+ if (!Array.isArray(node)) return `the text "${clip(node)}"`;
132
+ if (!isElement(node)) return "a comment";
133
+ const kind = KINDS.get(node[0]) ?? `\`${node[0]}\``;
134
+ const words = clip(textContent(node));
135
+ return `the ${kind}${words ? ` "${words}"` : ""}`;
136
+ }
137
+ function fileTree(node, name, issues) {
138
+ for (const key of Object.keys(node[1])) {
139
+ const prop = key.replace(/^:/, "");
140
+ if (prop !== "id" && prop !== "class") issues.push({
141
+ rule: "comark/file-tree/unknown-prop",
142
+ message: `${name}: \`${prop}\` isn't a prop of \`::file-tree\`, which takes none, so it's ignored. Remove it.`
143
+ });
144
+ }
145
+ const content = childrenOf(node).filter(isContent);
146
+ const [block] = content.filter((child) => isElement(child) && child[0] === "pre");
147
+ if (!isElement(block)) {
148
+ const list = content.find((child) => isElement(child) && /^[ou]l$/.test(child[0]));
149
+ issues.push(list ? {
150
+ rule: "comark/file-tree/list-instead-of-fence",
151
+ message: `${name} holds a Markdown list, which turns names like \`__init__.py\` bold and drops \`[id].vue\`. Write the tree in a fenced block.`
152
+ } : {
153
+ rule: "comark/file-tree/empty-tree",
154
+ message: `${name} has no tree. Write the tree in a fenced block inside it, one entry per line.`
155
+ });
156
+ return content;
157
+ }
158
+ for (const child of content) if (child !== block) issues.push({
159
+ rule: "comark/file-tree/content-dropped",
160
+ message: `${name}: only one fenced block goes inside, so ${describe(child)} doesn't render. Move it out of the block.`
161
+ });
162
+ const tree = readTree(textContent(block));
163
+ if (!tree.length) issues.push({
164
+ rule: "comark/file-tree/empty-tree",
165
+ message: `${name}'s fenced block is empty. Write one entry per line.`
166
+ });
167
+ node.splice(1, node.length - 1, {}, ...tree);
168
+ return [node];
169
+ }
170
+ /**
171
+ * Turns `::file-tree` around a fenced block, one entry per line indented by two spaces or pasted from `tree`, into a
172
+ * tree of folders and files. A trailing `/` makes a folder, `+`, `-`, and `~` mark an entry added, removed, or
173
+ * changed, `# note` ends a line with a note, and `...` stands for entries left out. Problems are added to `meta.issues`.
174
+ */
175
+ var src_default = defineComarkPlugin((options = {}) => {
176
+ const icons = Object.fromEntries(Object.entries({
177
+ ...ICONS,
178
+ ...options.icons
179
+ }).map(([key, icon]) => [key.toLowerCase(), icon]));
180
+ return {
181
+ name: "comark-file-tree",
182
+ async post(state) {
183
+ const { tree } = state;
184
+ const issues = [];
185
+ let count = 0;
186
+ const transform = (nodes) => nodes.flatMap((node) => {
187
+ if (!isElement(node)) return [node];
188
+ if (node[0] === "file-tree") return fileTree(node, `\`::file-tree\` ${++count}`, issues);
189
+ node.splice(2, node.length - 2, ...transform(childrenOf(node)));
190
+ return [node];
191
+ });
192
+ tree.nodes = transform(tree.nodes);
193
+ if (options.iconResolver) {
194
+ const all = [...items(tree.nodes)].filter((item) => item[1].type !== "more");
195
+ const keys = all.map((item) => iconKey(item, icons));
196
+ const resolved = await options.iconResolver.resolve(keys.filter((key) => Boolean(key)));
197
+ for (const [at, item] of all.entries()) {
198
+ const icon = resolved.get(keys[at] ?? "");
199
+ if (icon && "src" in icon) item[1].icon = icon;
200
+ }
201
+ }
202
+ tree.meta.issues = [...tree.meta.issues ?? [], ...issues.map((issue) => issue.message)];
203
+ tree.meta.issueRules = {
204
+ ...tree.meta.issueRules,
205
+ ...Object.fromEntries(issues.map((issue) => [issue.message, issue.rule]))
206
+ };
207
+ }
208
+ };
209
+ });
210
+ //#endregion
211
+ export { src_default as t };
package/dist/style.css CHANGED
@@ -1 +1 @@
1
- @layer comark{@supports (interpolate-size:allow-keywords) and selector(::details-content){@media screen and (prefers-reduced-motion:no-preference){.cft details{interpolate-size:allow-keywords}.cft details::details-content{display:flow-root;block-size:0;overflow-y:clip;transition:block-size .25s cubic-bezier(.23,1,.32,1),content-visibility .25s allow-discrete}.cft details[open]::details-content{block-size:auto}}}:where(.cft){--_comark-background:var(--comark-background,var(--ui-bg,var(--background,#fff)));--_comark-foreground:var(--comark-foreground,var(--ui-text-highlighted,var(--foreground,#0a0a0a)));--_comark-muted:var(--comark-muted,var(--ui-text-muted,var(--muted-foreground,#737373)));--_comark-border:var(--comark-border,var(--ui-border,var(--border,#e5e5e5)));--_comark-accent:var(--comark-accent,var(--ui-bg-elevated,var(--accent,#f5f5f5)));--_comark-ring:var(--comark-ring,var(--ui-primary,var(--ring,#a1a1a1)));--_comark-radius:var(--comark-radius,calc(var(--ui-radius,calc(var(--radius,.625rem)/2))*2));--_comark-font:var(--comark-font,var(--font-sans,ui-sans-serif,system-ui,sans-serif))}@media (prefers-color-scheme:dark){:where(.cft){--_comark-background:var(--comark-background,var(--ui-bg,var(--background,#0a0a0a)));--_comark-foreground:var(--comark-foreground,var(--ui-text-highlighted,var(--foreground,#fafafa)));--_comark-muted:var(--comark-muted,var(--ui-text-muted,var(--muted-foreground,#a1a1a1)));--_comark-border:var(--comark-border,var(--ui-border,var(--border,rgb(255 255 255/.1))));--_comark-accent:var(--comark-accent,var(--ui-bg-elevated,var(--accent,#262626)));--_comark-ring:var(--comark-ring,var(--ui-primary,var(--ring,#737373)))}}:where(.cft){--cft-added:#16a34a;--cft-removed:var(--destructive,#dc2626);--cft-changed:#d97706}@media (prefers-color-scheme:dark){:where(.cft){--cft-added:#4ade80;--cft-removed:var(--destructive,#f87171);--cft-changed:#fbbf24}}.cft{margin:1.5rem 0;padding:.75rem;overflow-x:auto;border:1px solid var(--_comark-border);border-radius:var(--_comark-radius);background:var(--_comark-background);color:var(--_comark-foreground);font:400 .8125rem/1.25rem ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.cft ul{margin:0!important;padding:0!important;list-style:none!important}.cft ul ul{margin-inline-start:calc(.5rem - .5px)!important;padding-inline-start:.75rem!important;border-inline-start:1px solid var(--_comark-border)}.cft li{margin:0!important;padding:0!important}.cft details{margin:0!important;padding:0!important}.cft summary{display:block;margin:0!important;padding:0!important;list-style:none;border-radius:calc(var(--_comark-radius) - 4px);cursor:pointer}.cft summary::-webkit-details-marker{display:none}.cft summary:hover{background:var(--_comark-accent)}.cft summary:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--_comark-ring) 50%,transparent)}.cft-entry{display:flex;align-items:center;gap:.375rem;padding:.125rem .25rem;white-space:nowrap}.cft-glyph{flex:none;width:1rem;height:1rem;color:var(--_comark-muted)}.cft-entry .ci{flex:none;font-size:1rem}.cft-note{margin-inline-start:.75rem;color:var(--_comark-muted);font-family:var(--_comark-font);white-space:normal}.cft-mark{font-weight:600}.cft [data-state='added']>.cft-entry,.cft [data-state='added']>details>summary>.cft-entry{color:var(--cft-added)}.cft [data-state='removed']>.cft-entry,.cft [data-state='removed']>details>summary>.cft-entry{color:var(--cft-removed)}.cft [data-state='removed'] .cft-name{text-decoration:line-through}.cft [data-state='changed']>.cft-entry,.cft [data-state='changed']>details>summary>.cft-entry{color:var(--cft-changed)}.cft [data-type='more']{padding:0 .25rem;color:var(--_comark-muted)}}
1
+ @layer comark{@supports (interpolate-size:allow-keywords) and selector(::details-content){@media screen and (prefers-reduced-motion:no-preference){.cft details{interpolate-size:allow-keywords}.cft details::details-content{display:flow-root;block-size:0;opacity:0;overflow-y:clip;transition:block-size .18s cubic-bezier(.23,1,.32,1),opacity .12s ease-in,content-visibility .18s allow-discrete}.cft details[open]::details-content{block-size:auto;opacity:1;transition:block-size .25s cubic-bezier(.23,1,.32,1),opacity .25s cubic-bezier(.23,1,.32,1),content-visibility .25s allow-discrete}}}:where(.cft){--_comark-background:var(--comark-background,var(--ui-bg,var(--background,#fff)));--_comark-foreground:var(--comark-foreground,var(--ui-text-highlighted,var(--foreground,#0a0a0a)));--_comark-muted:var(--comark-muted,var(--ui-text-muted,var(--muted-foreground,#737373)));--_comark-border:var(--comark-border,var(--ui-border,var(--border,#e5e5e5)));--_comark-accent:var(--comark-accent,var(--ui-bg-elevated,var(--accent,#f5f5f5)));--_comark-ring:var(--comark-ring,var(--ui-primary,var(--ring,#a1a1a1)));--_comark-success:var(--comark-success,var(--ui-success,var(--success,#259f56)));--_comark-danger:var(--comark-danger,var(--ui-error,var(--destructive,#ee3533)));--_comark-warning:var(--comark-warning,var(--ui-warning,#d97706));--_comark-info:var(--comark-info,var(--ui-info,#2563eb));--_comark-radius:var(--comark-radius,calc(var(--ui-radius,calc(var(--radius,.625rem)/2))*2));--_comark-font:var(--comark-font,var(--font-sans,ui-sans-serif,system-ui,sans-serif))}@media (prefers-color-scheme:dark){:where(.cft){--_comark-background:var(--comark-background,var(--ui-bg,var(--background,#0a0a0a)));--_comark-foreground:var(--comark-foreground,var(--ui-text-highlighted,var(--foreground,#fafafa)));--_comark-muted:var(--comark-muted,var(--ui-text-muted,var(--muted-foreground,#a1a1a1)));--_comark-border:var(--comark-border,var(--ui-border,var(--border,rgb(255 255 255/.1))));--_comark-accent:var(--comark-accent,var(--ui-bg-elevated,var(--accent,#262626)));--_comark-ring:var(--comark-ring,var(--ui-primary,var(--ring,#737373)));--_comark-success:var(--comark-success,var(--ui-success,var(--success,#59d38c)));--_comark-danger:var(--comark-danger,var(--ui-error,var(--destructive,#fd736d)));--_comark-warning:var(--comark-warning,var(--ui-warning,#fbbf24));--_comark-info:var(--comark-info,var(--ui-info,#60a5fa))}}:where(.light,.light *):where(.cft){--_comark-success:var(--comark-success,var(--ui-success,var(--success,#259f56)));--_comark-danger:var(--comark-danger,var(--ui-error,var(--destructive,#ee3533)));--_comark-warning:var(--comark-warning,var(--ui-warning,#d97706));--_comark-info:var(--comark-info,var(--ui-info,#2563eb))}:where(.dark,.dark *):where(.cft){--_comark-success:var(--comark-success,var(--ui-success,var(--success,#59d38c)));--_comark-danger:var(--comark-danger,var(--ui-error,var(--destructive,#fd736d)));--_comark-warning:var(--comark-warning,var(--ui-warning,#fbbf24));--_comark-info:var(--comark-info,var(--ui-info,#60a5fa))}:where(.cft){--cft-added:var(--_comark-success);--cft-removed:var(--_comark-danger);--cft-changed:var(--_comark-warning)}.cft{margin:1.5rem 0;padding:.75rem;overflow-x:auto;border:1px solid var(--_comark-border);border-radius:var(--_comark-radius);background:var(--_comark-background);color:var(--_comark-foreground);font:400 .8125rem/1.25rem ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}.cft ul{margin:0!important;padding:0!important;list-style:none!important}.cft ul ul{margin-inline-start:calc(.5rem - .5px)!important;padding-inline-start:.75rem!important;border-inline-start:1px solid var(--_comark-border)}.cft li{margin:0!important;padding:0!important}.cft details{margin:0!important;padding:0!important}.cft summary{display:block;margin:0!important;padding:0!important;list-style:none;border-radius:calc(var(--_comark-radius) - 4px);cursor:pointer}.cft summary::-webkit-details-marker{display:none}.cft summary:hover{background:var(--_comark-accent)}.cft summary:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--_comark-ring) 50%,transparent)}.cft-entry{display:flex;align-items:center;gap:.375rem;padding:.125rem .25rem;white-space:nowrap}.cft-glyph{flex:none;width:1rem;height:1rem;color:var(--_comark-muted)}.cft-entry .ci{flex:none;font-size:1rem}.cft-note{margin-inline-start:.75rem;color:var(--_comark-muted);font-family:var(--_comark-font);white-space:normal}.cft-mark{font-weight:600}.cft [data-state='added']>.cft-entry,.cft [data-state='added']>details>summary>.cft-entry{color:var(--cft-added)}.cft [data-state='removed']>.cft-entry,.cft [data-state='removed']>details>summary>.cft-entry{color:var(--cft-removed)}.cft [data-state='removed'] .cft-name{text-decoration:line-through}.cft [data-state='changed']>.cft-entry,.cft [data-state='changed']>details>summary>.cft-entry{color:var(--cft-changed)}.cft [data-type='more']{padding:0 .25rem;color:var(--_comark-muted)}}
package/dist/vue.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import src_default from "./index.mjs";
2
- import { n as glyphs, r as marks, t as addStyles } from "./client-Djg5meKD.mjs";
1
+ import { t as src_default } from "./src-PLku93wQ.mjs";
2
+ import { n as glyphs, r as marks, t as addStyles } from "./client-CmwF7cCV.mjs";
3
3
  import { defineComponent, h, onMounted } from "vue";
4
4
  import { Icon } from "comark-icon/vue";
5
5
  //#region src/vue.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "comark-file-tree",
3
- "version": "0.7.6",
3
+ "version": "0.8.1",
4
4
  "description": "A project's folders and files from an indented list or pasted tree output, with added, removed, and changed entries marked. A Comark plugin.",
5
5
  "keywords": [
6
6
  "comark",
@@ -45,7 +45,7 @@
45
45
  "provenance": true
46
46
  },
47
47
  "dependencies": {
48
- "comark-icon": "^0.7.6"
48
+ "comark-icon": "^0.8.1"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@comark/html": "^0.8.1",
@@ -58,7 +58,7 @@
58
58
  "react-dom": "^19.3.1",
59
59
  "svelte": "^5.57.2",
60
60
  "vue": "^3.5.44",
61
- "comark-zoom": "0.7.6"
61
+ "comark-zoom": "0.8.1"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "@comark/html": "^0.8.0",