comark-file-tree 0.0.0 → 0.7.2
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/LICENSE +69 -0
- package/README.md +47 -1
- package/dist/client-C13QjLJq.mjs +113 -0
- package/dist/client.d.mts +22 -0
- package/dist/client.mjs +1 -0
- package/dist/html.d.mts +7 -0
- package/dist/html.mjs +30 -0
- package/dist/index.d.mts +41 -0
- package/dist/index.mjs +209 -0
- package/dist/react.d.mts +29 -0
- package/dist/react.mjs +40 -0
- package/dist/style.css +1 -0
- package/dist/svelte/FileTree.svelte +10 -0
- package/dist/svelte/FileTreeItem.svelte +28 -0
- package/dist/svelte/index.d.ts +21 -0
- package/dist/svelte/index.js +7 -0
- package/dist/vue.d.mts +47 -0
- package/dist/vue.mjs +62 -0
- package/package.json +84 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 onmax
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
The icons are Lucide's (https://lucide.dev), under its license:
|
|
26
|
+
|
|
27
|
+
ISC License
|
|
28
|
+
|
|
29
|
+
Copyright (c) 2026 Lucide Icons and Contributors
|
|
30
|
+
|
|
31
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
32
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
33
|
+
copyright notice and this permission notice appear in all copies.
|
|
34
|
+
|
|
35
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
36
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
37
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
38
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
39
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
40
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
41
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
The following Lucide icons are derived from the Feather project:
|
|
46
|
+
|
|
47
|
+
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
|
|
48
|
+
|
|
49
|
+
The MIT License (MIT) (for the icons listed above)
|
|
50
|
+
|
|
51
|
+
Copyright (c) 2013-present Cole Bemis
|
|
52
|
+
|
|
53
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
54
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
55
|
+
in the Software without restriction, including without limitation the rights
|
|
56
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
57
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
58
|
+
furnished to do so, subject to the following conditions:
|
|
59
|
+
|
|
60
|
+
The above copyright notice and this permission notice shall be included in all
|
|
61
|
+
copies or substantial portions of the Software.
|
|
62
|
+
|
|
63
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
64
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
65
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
66
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
67
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
68
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
69
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
1
|
# comark-file-tree
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://npmx.dev/package/comark-file-tree)
|
|
4
|
+
[](https://npmx.dev/package/comark-file-tree)
|
|
5
|
+
[](https://npmx.dev/package/comark-file-tree)
|
|
6
|
+
[](https://comark-ui.onmax.me/file-tree/)
|
|
7
|
+
|
|
8
|
+
A project's folders and files from an indented list in a fenced block, or pasted `tree` output, with added, removed, and changed entries marked and a note on any line. Folders are open `<details>`, so there's no script. A [Comark](https://comark.dev) plugin for HTML, Vue, React, and Svelte.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add comark comark-file-tree
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { createHtmlRenderer } from "@comark/html";
|
|
16
|
+
import fileTree, { components } from "comark-file-tree/html";
|
|
17
|
+
|
|
18
|
+
const render = createHtmlRenderer({ plugins: [fileTree()], components });
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
````md
|
|
22
|
+
::file-tree
|
|
23
|
+
```
|
|
24
|
+
src/
|
|
25
|
+
+ new.ts # added in this change
|
|
26
|
+
- old.ts
|
|
27
|
+
~ app.vue
|
|
28
|
+
...
|
|
29
|
+
README.md
|
|
30
|
+
```
|
|
31
|
+
::
|
|
32
|
+
````
|
|
33
|
+
|
|
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
|
+
|
|
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.
|
|
37
|
+
|
|
38
|
+
## Renderers
|
|
39
|
+
|
|
40
|
+
| Renderer | Import |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| `@comark/html` | `comark-file-tree/html`: the styles are inlined once per document |
|
|
43
|
+
| `@comark/vue` | `comark-file-tree/vue` |
|
|
44
|
+
| `@comark/react` | `comark-file-tree/react` |
|
|
45
|
+
| `@comark/svelte` | `comark-file-tree/svelte` |
|
|
46
|
+
|
|
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
|
+
|
|
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.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//#region ../zoom/dist/theme.mjs
|
|
2
|
+
const tokens = [
|
|
3
|
+
[
|
|
4
|
+
"background",
|
|
5
|
+
"ui-bg",
|
|
6
|
+
"background",
|
|
7
|
+
"#fff",
|
|
8
|
+
"#0a0a0a"
|
|
9
|
+
],
|
|
10
|
+
[
|
|
11
|
+
"foreground",
|
|
12
|
+
"ui-text-highlighted",
|
|
13
|
+
"foreground",
|
|
14
|
+
"#0a0a0a",
|
|
15
|
+
"#fafafa"
|
|
16
|
+
],
|
|
17
|
+
[
|
|
18
|
+
"muted",
|
|
19
|
+
"ui-text-muted",
|
|
20
|
+
"muted-foreground",
|
|
21
|
+
"#737373",
|
|
22
|
+
"#a1a1a1"
|
|
23
|
+
],
|
|
24
|
+
[
|
|
25
|
+
"border",
|
|
26
|
+
"ui-border",
|
|
27
|
+
"border",
|
|
28
|
+
"#e5e5e5",
|
|
29
|
+
"rgb(255 255 255/.1)"
|
|
30
|
+
],
|
|
31
|
+
[
|
|
32
|
+
"accent",
|
|
33
|
+
"ui-bg-elevated",
|
|
34
|
+
"accent",
|
|
35
|
+
"#f5f5f5",
|
|
36
|
+
"#262626"
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
"ring",
|
|
40
|
+
"ui-primary",
|
|
41
|
+
"ring",
|
|
42
|
+
"#a1a1a1",
|
|
43
|
+
"#737373"
|
|
44
|
+
]
|
|
45
|
+
];
|
|
46
|
+
const resolve = (scheme) => tokens.map((token) => `--_comark-${token[0]}:var(--comark-${token[0]},var(--${token[1]},var(--${token[2]},${token[scheme]})))`).join(";");
|
|
47
|
+
/**
|
|
48
|
+
* Resolves the `--comark-*` tokens on `selector`, falling back to Nuxt UI's variables, then shadcn/ui's, then shadcn's
|
|
49
|
+
* neutral theme. Nuxt UI's corners are twice its `--ui-radius`, so the radius halves shadcn's to double it back.
|
|
50
|
+
* They resolve on the component rather than `:root`, so a `.dark` class anywhere above it switches the theme, and a
|
|
51
|
+
* `--comark-*` set on any ancestor still inherits down. `:where()` keeps them at zero specificity.
|
|
52
|
+
*/
|
|
53
|
+
function theme(selector) {
|
|
54
|
+
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)}}}`;
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/styles.ts
|
|
59
|
+
/**
|
|
60
|
+
* A tree in a bordered box, with a guide line down each open folder, on the shared `--comark-*` tokens. Added, removed,
|
|
61
|
+
* and changed entries take `--cft-added`, `--cft-removed`, and `--cft-changed`; removed defaults to shadcn/ui's
|
|
62
|
+
* `--destructive`. The rules sit in the `comark` layer, so any unlayered rule of the host's wins.
|
|
63
|
+
*/
|
|
64
|
+
const css = `@layer comark{${[
|
|
65
|
+
theme(".cft"),
|
|
66
|
+
":where(.cft){--cft-added:#16a34a;--cft-removed:var(--destructive,#dc2626);--cft-changed:#d97706}",
|
|
67
|
+
"@media (prefers-color-scheme:dark){:where(.cft){--cft-added:#4ade80;--cft-removed:var(--destructive,#f87171);--cft-changed:#fbbf24}}",
|
|
68
|
+
".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}",
|
|
69
|
+
".cft ul{margin:0!important;padding:0!important;list-style:none!important}",
|
|
70
|
+
".cft ul ul{margin-inline-start:calc(.5rem - .5px)!important;padding-inline-start:.75rem!important;border-inline-start:1px solid var(--_comark-border)}",
|
|
71
|
+
".cft li{margin:0!important;padding:0!important}",
|
|
72
|
+
".cft details{margin:0!important;padding:0!important}",
|
|
73
|
+
".cft summary{display:block;margin:0!important;padding:0!important;list-style:none;border-radius:calc(var(--_comark-radius) - 4px);cursor:pointer}",
|
|
74
|
+
".cft summary::-webkit-details-marker{display:none}",
|
|
75
|
+
".cft summary:hover{background:var(--_comark-accent)}",
|
|
76
|
+
".cft summary:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--_comark-ring) 50%,transparent)}",
|
|
77
|
+
".cft-entry{display:flex;align-items:center;gap:.375rem;padding:.125rem .25rem;white-space:nowrap}",
|
|
78
|
+
".cft-glyph{flex:none;width:1rem;height:1rem;color:var(--_comark-muted)}",
|
|
79
|
+
".cft-entry .ci{flex:none;font-size:1rem}",
|
|
80
|
+
".cft-note{margin-inline-start:.75rem;color:var(--_comark-muted);font-family:var(--_comark-font);white-space:normal}",
|
|
81
|
+
".cft-mark{font-weight:600}",
|
|
82
|
+
".cft [data-state='added']>.cft-entry,.cft [data-state='added']>details>summary>.cft-entry{color:var(--cft-added)}",
|
|
83
|
+
".cft [data-state='removed']>.cft-entry,.cft [data-state='removed']>details>summary>.cft-entry{color:var(--cft-removed)}",
|
|
84
|
+
".cft [data-state='removed'] .cft-name{text-decoration:line-through}",
|
|
85
|
+
".cft [data-state='changed']>.cft-entry,.cft [data-state='changed']>details>summary>.cft-entry{color:var(--cft-changed)}",
|
|
86
|
+
".cft [data-type='more']{padding:0 .25rem;color:var(--_comark-muted)}"
|
|
87
|
+
].join("")}}`;
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/client.ts
|
|
90
|
+
/** Adds the file tree styles to the page once. The Vue, React, and Svelte components call it when they mount. */
|
|
91
|
+
function addStyles() {
|
|
92
|
+
if (document.querySelector("style[data-comark-file-tree]")) return;
|
|
93
|
+
const style = document.createElement("style");
|
|
94
|
+
style.dataset.comarkFileTree = "";
|
|
95
|
+
style.textContent = css;
|
|
96
|
+
document.head.append(style);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Lucide's folder and file (ISC), each as one path, for entries without an icon. Every renderer draws them in the
|
|
100
|
+
* same `<svg>`, sized for a page without the styles.
|
|
101
|
+
*/
|
|
102
|
+
const glyphs = {
|
|
103
|
+
folder: "M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",
|
|
104
|
+
file: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7ZM14 2v4a2 2 0 0 0 2 2h4"
|
|
105
|
+
};
|
|
106
|
+
/** The sign before an entry's name, so its change reads without color. */
|
|
107
|
+
const marks = /* @__PURE__ */ new Map([
|
|
108
|
+
["added", "+"],
|
|
109
|
+
["removed", "-"],
|
|
110
|
+
["changed", "~"]
|
|
111
|
+
]);
|
|
112
|
+
//#endregion
|
|
113
|
+
export { css as i, glyphs as n, marks as r, addStyles as t };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//#region src/styles.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A tree in a bordered box, with a guide line down each open folder, on the shared `--comark-*` tokens. Added, removed,
|
|
4
|
+
* and changed entries take `--cft-added`, `--cft-removed`, and `--cft-changed`; removed defaults to shadcn/ui's
|
|
5
|
+
* `--destructive`. The rules sit in the `comark` layer, so any unlayered rule of the host's wins.
|
|
6
|
+
*/
|
|
7
|
+
export declare const css: string;
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region src/client.d.ts
|
|
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;
|
|
12
|
+
/**
|
|
13
|
+
* Lucide's folder and file (ISC), each as one path, for entries without an icon. Every renderer draws them in the
|
|
14
|
+
* same `<svg>`, sized for a page without the styles.
|
|
15
|
+
*/
|
|
16
|
+
export declare const glyphs: {
|
|
17
|
+
folder: string;
|
|
18
|
+
file: string;
|
|
19
|
+
};
|
|
20
|
+
/** The sign before an entry's name, so its change reads without color. */
|
|
21
|
+
export declare const marks: Map<string, string>;
|
|
22
|
+
//#endregion
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { t as addStyles, i as css, n as glyphs, r as marks } from "./client-C13QjLJq.mjs";
|
package/dist/html.d.mts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import _default, { EntryState, FileTreeItemAttributes, FileTreeMeta, FileTreeOptions } from "./index.mjs";
|
|
2
|
+
import { NodeHandler } from "comark";
|
|
3
|
+
//#region src/html.d.ts
|
|
4
|
+
/** The HTML renderer for `::file-tree`. Pass it as `components`; it inlines the styles once per document. */
|
|
5
|
+
export declare const components: Record<"file-tree" | "file-tree-item", NodeHandler>;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { type EntryState, type FileTreeItemAttributes, type FileTreeMeta, type FileTreeOptions, _default as default };
|
package/dist/html.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import src_default from "./index.mjs";
|
|
2
|
+
import { i as css, n as glyphs, r as marks } from "./client-C13QjLJq.mjs";
|
|
3
|
+
import { escapeHtml } from "comark/utils";
|
|
4
|
+
import { iconHtml, iconStyles } from "comark-icon/html";
|
|
5
|
+
//#region src/html.ts
|
|
6
|
+
const styled = /* @__PURE__ */ new WeakSet();
|
|
7
|
+
const text = (value) => value === void 0 ? "" : escapeHtml(String(value));
|
|
8
|
+
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
|
+
const hasIcon = (node) => Array.isArray(node) && (Boolean(node[1]?.icon) || node.slice(2).some(hasIcon));
|
|
10
|
+
const fileTree = async (node, state) => {
|
|
11
|
+
const styles = styled.has(state.context) ? "" : `<style data-comark-file-tree>${css}</style>`;
|
|
12
|
+
styled.add(state.context);
|
|
13
|
+
return `${styles}${hasIcon(node) ? iconStyles(state) : ""}<div class="cft"><ul>${await state.flow(node, state)}</ul></div>`;
|
|
14
|
+
};
|
|
15
|
+
const item = async (node, state) => {
|
|
16
|
+
const [, attrs] = node;
|
|
17
|
+
if (attrs.type === "more") return "<li data-type=\"more\">…</li>";
|
|
18
|
+
const folder = attrs.type === "folder";
|
|
19
|
+
const mark = marks.get(String(attrs.state));
|
|
20
|
+
const entry = `<span class="cft-entry">${attrs.icon ? iconHtml(attrs.icon) : glyph(folder ? glyphs.folder : glyphs.file)}${mark ? `<span class="cft-mark">${mark}</span>` : ""}<span class="cft-name">${text(attrs.name)}</span>${attrs.note ? `<span class="cft-note">${text(attrs.note)}</span>` : ""}</span>`;
|
|
21
|
+
const open = `<li data-type="${folder ? "folder" : "file"}"${mark ? ` data-state="${text(attrs.state)}"` : ""}>`;
|
|
22
|
+
return folder ? `${open}<details open><summary>${entry}</summary><ul>${await state.flow(node, state)}</ul></details></li>` : `${open}${entry}</li>`;
|
|
23
|
+
};
|
|
24
|
+
/** The HTML renderer for `::file-tree`. Pass it as `components`; it inlines the styles once per document. */
|
|
25
|
+
const components = {
|
|
26
|
+
"file-tree": fileTree,
|
|
27
|
+
"file-tree-item": item
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
export { components, src_default as default };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { IconData, Icons } from "comark-icon";
|
|
2
|
+
//#region src/index.d.ts
|
|
3
|
+
export type FileTreeOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* Resolves file and folder icons, from `createIcons()` in comark-icon. Without it, files and folders get the same
|
|
6
|
+
* plain glyph each and nothing is fetched. Pass the one the page's other plugins use, so they share its `collections`.
|
|
7
|
+
*/
|
|
8
|
+
iconResolver?: Icons;
|
|
9
|
+
/**
|
|
10
|
+
* Icons by file name, such as `{ "nuxt.config.ts": "vscode-icons:file-type-nuxt" }`, or by extension, such as
|
|
11
|
+
* `{ ".vue": "logos:vue" }`, on top of the built-in ones. `folder` and `file` set the fallbacks.
|
|
12
|
+
*/
|
|
13
|
+
icons?: Record<string, string>;
|
|
14
|
+
};
|
|
15
|
+
export type FileTreeMeta = {
|
|
16
|
+
/** One readable sentence per problem with a `::file-tree`. Empty when the document has none. Other plugins add theirs too. */
|
|
17
|
+
issues: string[];
|
|
18
|
+
/** The comark-linter rule of each issue, by its sentence, so the linter reports it under its own code. */
|
|
19
|
+
issueRules: Record<string, string>;
|
|
20
|
+
};
|
|
21
|
+
/** How an entry changed: written as `+`, `-`, or `~` before its name. */
|
|
22
|
+
export type EntryState = "added" | "removed" | "changed";
|
|
23
|
+
/**
|
|
24
|
+
* What each line of a tree becomes, as a `file-tree-item` node whose children are its entries. A `more` entry is a
|
|
25
|
+
* `...` placeholder and has no name.
|
|
26
|
+
*/
|
|
27
|
+
export type FileTreeItemAttributes = {
|
|
28
|
+
type?: "folder" | "file" | "more";
|
|
29
|
+
name?: string;
|
|
30
|
+
state?: EntryState;
|
|
31
|
+
note?: string;
|
|
32
|
+
icon?: IconData;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Turns `::file-tree` around a fenced block, one entry per line indented by two spaces or pasted from `tree`, into a
|
|
36
|
+
* tree of folders and files. A trailing `/` makes a folder, `+`, `-`, and `~` mark an entry added, removed, or
|
|
37
|
+
* changed, `# note` ends a line with a note, and `...` stands for entries left out. Problems are added to `meta.issues`.
|
|
38
|
+
*/
|
|
39
|
+
declare const _default: import("comark").ComarkPluginFactory<FileTreeOptions, FileTreeMeta, {}>;
|
|
40
|
+
//#endregion
|
|
41
|
+
export { _default as default };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
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 };
|
package/dist/react.d.mts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import _default, { EntryState, FileTreeItemAttributes, FileTreeMeta, FileTreeOptions } from "./index.mjs";
|
|
2
|
+
import { IconData } from "comark-icon/react";
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
//#region src/react.d.ts
|
|
5
|
+
/** `::file-tree` for `@comark/react`. */
|
|
6
|
+
export declare function FileTree({ children }: {
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
}): import("react").DetailedReactHTMLElement<{
|
|
9
|
+
className: string;
|
|
10
|
+
}, HTMLElement>;
|
|
11
|
+
type FileTreeItemProps = {
|
|
12
|
+
type?: string;
|
|
13
|
+
name?: string;
|
|
14
|
+
state?: string;
|
|
15
|
+
note?: string;
|
|
16
|
+
icon?: IconData;
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
};
|
|
19
|
+
/** Each entry of a `::file-tree` for `@comark/react`. */
|
|
20
|
+
export declare function FileTreeItem({ type, name, state, note, icon, children }: FileTreeItemProps): import("react").ReactElement<{
|
|
21
|
+
"data-type": string;
|
|
22
|
+
}, string | import("react").JSXElementConstructor<any>>;
|
|
23
|
+
/** Pass these as `components` to `@comark/react`. */
|
|
24
|
+
export declare const components: {
|
|
25
|
+
FileTree: typeof FileTree;
|
|
26
|
+
FileTreeItem: typeof FileTreeItem;
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { type EntryState, type FileTreeItemAttributes, type FileTreeMeta, type FileTreeOptions, _default as default };
|
package/dist/react.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import src_default from "./index.mjs";
|
|
2
|
+
import { n as glyphs, r as marks, t as addStyles } from "./client-C13QjLJq.mjs";
|
|
3
|
+
import { Icon } from "comark-icon/react";
|
|
4
|
+
import { createElement, useEffect } from "react";
|
|
5
|
+
//#region src/react.ts
|
|
6
|
+
const glyph = (d) => createElement("svg", {
|
|
7
|
+
className: "cft-glyph",
|
|
8
|
+
viewBox: "0 0 24 24",
|
|
9
|
+
width: 16,
|
|
10
|
+
height: 16,
|
|
11
|
+
fill: "none",
|
|
12
|
+
stroke: "currentColor",
|
|
13
|
+
strokeWidth: 2,
|
|
14
|
+
strokeLinecap: "round",
|
|
15
|
+
strokeLinejoin: "round",
|
|
16
|
+
"aria-hidden": true
|
|
17
|
+
}, createElement("path", { d }));
|
|
18
|
+
/** `::file-tree` for `@comark/react`. */
|
|
19
|
+
function FileTree({ children }) {
|
|
20
|
+
useEffect(addStyles, []);
|
|
21
|
+
return createElement("div", { className: "cft" }, createElement("ul", null, children));
|
|
22
|
+
}
|
|
23
|
+
/** Each entry of a `::file-tree` for `@comark/react`. */
|
|
24
|
+
function FileTreeItem({ type, name, state, note, icon, children }) {
|
|
25
|
+
if (type === "more") return createElement("li", { "data-type": "more" }, "…");
|
|
26
|
+
const folder = type === "folder";
|
|
27
|
+
const mark = marks.get(String(state));
|
|
28
|
+
const entry = createElement("span", { className: "cft-entry" }, icon?.src ? createElement(Icon, icon) : glyph(folder ? glyphs.folder : glyphs.file), mark ? createElement("span", { className: "cft-mark" }, mark) : null, createElement("span", { className: "cft-name" }, name), note ? createElement("span", { className: "cft-note" }, note) : null);
|
|
29
|
+
return createElement("li", {
|
|
30
|
+
"data-type": folder ? "folder" : "file",
|
|
31
|
+
"data-state": mark ? state : void 0
|
|
32
|
+
}, folder ? createElement("details", { open: true }, createElement("summary", null, entry), createElement("ul", null, children)) : entry);
|
|
33
|
+
}
|
|
34
|
+
/** Pass these as `components` to `@comark/react`. */
|
|
35
|
+
const components = {
|
|
36
|
+
FileTree,
|
|
37
|
+
FileTreeItem
|
|
38
|
+
};
|
|
39
|
+
//#endregion
|
|
40
|
+
export { FileTree, FileTreeItem, components, src_default as default };
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer comark{: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)}}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<!-- `::file-tree` for `@comark/svelte`. -->
|
|
2
|
+
<script>
|
|
3
|
+
import { onMount } from "svelte";
|
|
4
|
+
import { addStyles } from "comark-file-tree/client";
|
|
5
|
+
|
|
6
|
+
let { children } = $props();
|
|
7
|
+
onMount(addStyles);
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<div class="cft"><ul>{@render children?.()}</ul></div>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!-- Each entry of a `::file-tree` for `@comark/svelte`. -->
|
|
2
|
+
<script>
|
|
3
|
+
import { glyphs, marks } from "comark-file-tree/client";
|
|
4
|
+
import { Icon } from "comark-icon/svelte";
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
type = undefined,
|
|
8
|
+
name = undefined,
|
|
9
|
+
state = undefined,
|
|
10
|
+
note = undefined,
|
|
11
|
+
icon = undefined,
|
|
12
|
+
children,
|
|
13
|
+
} = $props();
|
|
14
|
+
const folder = $derived(type === "folder");
|
|
15
|
+
const mark = $derived(marks.get(String(state)));
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
{#snippet entry()}
|
|
19
|
+
<span class="cft-entry">{#if icon?.src}<Icon {...icon} />{:else}<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={folder ? glyphs.folder : glyphs.file} /></svg>{/if}{#if mark}<span class="cft-mark">{mark}</span>{/if}<span class="cft-name">{name}</span>{#if note}<span class="cft-note">{note}</span>{/if}</span>
|
|
20
|
+
{/snippet}
|
|
21
|
+
|
|
22
|
+
{#if type === "more"}
|
|
23
|
+
<li data-type="more">…</li>
|
|
24
|
+
{:else if folder}
|
|
25
|
+
<li data-type="folder" data-state={mark ? state : undefined}><details open><summary>{@render entry()}</summary><ul>{@render children?.()}</ul></details></li>
|
|
26
|
+
{:else}
|
|
27
|
+
<li data-type="file" data-state={mark ? state : undefined}>{@render entry()}</li>
|
|
28
|
+
{/if}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Component, Snippet } from "svelte";
|
|
2
|
+
import type { IconData } from "comark-icon";
|
|
3
|
+
|
|
4
|
+
export { default } from "comark-file-tree";
|
|
5
|
+
export type {
|
|
6
|
+
EntryState,
|
|
7
|
+
FileTreeItemAttributes,
|
|
8
|
+
FileTreeMeta,
|
|
9
|
+
FileTreeOptions,
|
|
10
|
+
} from "comark-file-tree";
|
|
11
|
+
|
|
12
|
+
export declare const FileTree: Component<{ children?: Snippet }>;
|
|
13
|
+
export declare const FileTreeItem: Component<{
|
|
14
|
+
type?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
state?: string;
|
|
17
|
+
note?: string;
|
|
18
|
+
icon?: IconData;
|
|
19
|
+
children?: Snippet;
|
|
20
|
+
}>;
|
|
21
|
+
export declare const components: { FileTree: typeof FileTree; FileTreeItem: typeof FileTreeItem };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// `::file-tree` for `@comark/svelte`. Pass `components` to it; the parse plugin comes from `comark-file-tree`.
|
|
2
|
+
import FileTree from "./FileTree.svelte";
|
|
3
|
+
import FileTreeItem from "./FileTreeItem.svelte";
|
|
4
|
+
|
|
5
|
+
export { default } from "comark-file-tree";
|
|
6
|
+
export { FileTree, FileTreeItem };
|
|
7
|
+
export const components = { FileTree, FileTreeItem };
|
package/dist/vue.d.mts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import _default, { EntryState, FileTreeItemAttributes, FileTreeMeta, FileTreeOptions } from "./index.mjs";
|
|
2
|
+
import { PropType } from "vue";
|
|
3
|
+
import { IconData } from "comark-icon/vue";
|
|
4
|
+
//#region src/vue.d.ts
|
|
5
|
+
/** `::file-tree` for `@comark/vue`. */
|
|
6
|
+
export declare const FileTree: import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
9
|
+
/** Each entry of a `::file-tree` for `@comark/vue`. */
|
|
10
|
+
export declare const FileTreeItem: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
11
|
+
type: StringConstructor;
|
|
12
|
+
name: StringConstructor;
|
|
13
|
+
state: StringConstructor;
|
|
14
|
+
note: StringConstructor;
|
|
15
|
+
icon: PropType<IconData>;
|
|
16
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
17
|
+
[key: string]: any;
|
|
18
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
19
|
+
type: StringConstructor;
|
|
20
|
+
name: StringConstructor;
|
|
21
|
+
state: StringConstructor;
|
|
22
|
+
note: StringConstructor;
|
|
23
|
+
icon: PropType<IconData>;
|
|
24
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
25
|
+
/** Pass these as `components` to `@comark/vue`. */
|
|
26
|
+
export declare const components: {
|
|
27
|
+
FileTree: import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
28
|
+
[key: string]: any;
|
|
29
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
30
|
+
FileTreeItem: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
31
|
+
type: StringConstructor;
|
|
32
|
+
name: StringConstructor;
|
|
33
|
+
state: StringConstructor;
|
|
34
|
+
note: StringConstructor;
|
|
35
|
+
icon: PropType<IconData>;
|
|
36
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
37
|
+
[key: string]: any;
|
|
38
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
39
|
+
type: StringConstructor;
|
|
40
|
+
name: StringConstructor;
|
|
41
|
+
state: StringConstructor;
|
|
42
|
+
note: StringConstructor;
|
|
43
|
+
icon: PropType<IconData>;
|
|
44
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
45
|
+
};
|
|
46
|
+
//#endregion
|
|
47
|
+
export { type EntryState, type FileTreeItemAttributes, type FileTreeMeta, type FileTreeOptions, _default as default };
|
package/dist/vue.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import src_default from "./index.mjs";
|
|
2
|
+
import { n as glyphs, r as marks, t as addStyles } from "./client-C13QjLJq.mjs";
|
|
3
|
+
import { defineComponent, h, onMounted } from "vue";
|
|
4
|
+
import { Icon } from "comark-icon/vue";
|
|
5
|
+
//#region src/vue.ts
|
|
6
|
+
const glyph = (d) => h("svg", {
|
|
7
|
+
class: "cft-glyph",
|
|
8
|
+
viewBox: "0 0 24 24",
|
|
9
|
+
width: "16",
|
|
10
|
+
height: "16",
|
|
11
|
+
fill: "none",
|
|
12
|
+
stroke: "currentColor",
|
|
13
|
+
"stroke-width": "2",
|
|
14
|
+
"stroke-linecap": "round",
|
|
15
|
+
"stroke-linejoin": "round",
|
|
16
|
+
"aria-hidden": "true"
|
|
17
|
+
}, [h("path", { d })]);
|
|
18
|
+
/** `::file-tree` for `@comark/vue`. */
|
|
19
|
+
const FileTree = defineComponent({
|
|
20
|
+
name: "FileTree",
|
|
21
|
+
inheritAttrs: false,
|
|
22
|
+
setup(_, { slots }) {
|
|
23
|
+
onMounted(addStyles);
|
|
24
|
+
return () => h("div", { class: "cft" }, [h("ul", slots.default?.())]);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
/** Each entry of a `::file-tree` for `@comark/vue`. */
|
|
28
|
+
const FileTreeItem = defineComponent({
|
|
29
|
+
name: "FileTreeItem",
|
|
30
|
+
inheritAttrs: false,
|
|
31
|
+
props: {
|
|
32
|
+
type: String,
|
|
33
|
+
name: String,
|
|
34
|
+
state: String,
|
|
35
|
+
note: String,
|
|
36
|
+
icon: Object
|
|
37
|
+
},
|
|
38
|
+
setup(props, { slots }) {
|
|
39
|
+
return () => {
|
|
40
|
+
if (props.type === "more") return h("li", { "data-type": "more" }, "…");
|
|
41
|
+
const folder = props.type === "folder";
|
|
42
|
+
const mark = marks.get(String(props.state));
|
|
43
|
+
const entry = h("span", { class: "cft-entry" }, [
|
|
44
|
+
props.icon?.src ? h(Icon, { ...props.icon }) : glyph(folder ? glyphs.folder : glyphs.file),
|
|
45
|
+
mark ? h("span", { class: "cft-mark" }, mark) : null,
|
|
46
|
+
h("span", { class: "cft-name" }, props.name),
|
|
47
|
+
props.note ? h("span", { class: "cft-note" }, props.note) : null
|
|
48
|
+
]);
|
|
49
|
+
return h("li", {
|
|
50
|
+
"data-type": folder ? "folder" : "file",
|
|
51
|
+
"data-state": mark ? props.state : void 0
|
|
52
|
+
}, folder ? [h("details", { open: true }, [h("summary", [entry]), h("ul", slots.default?.())])] : [entry]);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
/** Pass these as `components` to `@comark/vue`. */
|
|
57
|
+
const components = {
|
|
58
|
+
FileTree,
|
|
59
|
+
FileTreeItem
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
62
|
+
export { FileTree, FileTreeItem, components, src_default as default };
|
package/package.json
CHANGED
|
@@ -1,12 +1,92 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comark-file-tree",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.7.2",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"comark",
|
|
7
|
+
"file-tree",
|
|
8
|
+
"markdown",
|
|
9
|
+
"mdc",
|
|
10
|
+
"plugin",
|
|
11
|
+
"tree"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://comark-ui.onmax.me/file-tree",
|
|
14
|
+
"bugs": "https://github.com/onmax/comark-ui/issues",
|
|
5
15
|
"license": "MIT",
|
|
6
16
|
"author": "onmax",
|
|
7
17
|
"repository": {
|
|
8
18
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/onmax/comark-
|
|
19
|
+
"url": "git+https://github.com/onmax/comark-ui.git",
|
|
10
20
|
"directory": "packages/file-tree"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"sideEffects": [
|
|
27
|
+
"*.css"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": "./dist/index.mjs",
|
|
31
|
+
"./client": "./dist/client.mjs",
|
|
32
|
+
"./html": "./dist/html.mjs",
|
|
33
|
+
"./react": "./dist/react.mjs",
|
|
34
|
+
"./style.css": "./dist/style.css",
|
|
35
|
+
"./svelte": {
|
|
36
|
+
"types": "./dist/svelte/index.d.ts",
|
|
37
|
+
"svelte": "./dist/svelte/index.js",
|
|
38
|
+
"default": "./dist/svelte/index.js"
|
|
39
|
+
},
|
|
40
|
+
"./vue": "./dist/vue.mjs",
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public",
|
|
45
|
+
"provenance": true
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"comark-icon": "^0.7.2"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@comark/html": "^0.8.1",
|
|
52
|
+
"@comark/react": "^0.8.1",
|
|
53
|
+
"@comark/vue": "^0.8.1",
|
|
54
|
+
"@types/react": "^19.3.0",
|
|
55
|
+
"@types/react-dom": "^19.3.0",
|
|
56
|
+
"comark": "^0.8.1",
|
|
57
|
+
"react": "^19.3.1",
|
|
58
|
+
"react-dom": "^19.3.1",
|
|
59
|
+
"svelte": "^5.57.2",
|
|
60
|
+
"vue": "^3.5.44",
|
|
61
|
+
"comark-zoom": "0.7.2"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"@comark/html": "^0.8.0",
|
|
65
|
+
"comark": "^0.8.0",
|
|
66
|
+
"react": ">=18",
|
|
67
|
+
"svelte": ">=5",
|
|
68
|
+
"vue": ">=3.5"
|
|
69
|
+
},
|
|
70
|
+
"peerDependenciesMeta": {
|
|
71
|
+
"@comark/html": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"react": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
77
|
+
"svelte": {
|
|
78
|
+
"optional": true
|
|
79
|
+
},
|
|
80
|
+
"vue": {
|
|
81
|
+
"optional": true
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
"engines": {
|
|
85
|
+
"node": ">=22.12.0"
|
|
86
|
+
},
|
|
87
|
+
"scripts": {
|
|
88
|
+
"build": "tsdown",
|
|
89
|
+
"dev": "tsdown --watch",
|
|
90
|
+
"doctor": "vite-doctor . --no-cache --max-warnings 0 --format text"
|
|
11
91
|
}
|
|
12
|
-
}
|
|
92
|
+
}
|