comark-table 0.0.0-stage → 0.7.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/LICENSE +69 -0
- package/README.md +49 -2
- package/dist/client-C5bRu9NG.mjs +275 -0
- package/dist/client.d.mts +29 -0
- package/dist/client.mjs +1 -0
- package/dist/html.d.mts +7 -0
- package/dist/html.mjs +20 -0
- package/dist/index.d.mts +40 -0
- package/dist/index.mjs +433 -0
- package/dist/react.d.mts +15 -0
- package/dist/react.mjs +16 -0
- package/dist/style.css +1 -0
- package/dist/svelte/DataTable.svelte +16 -0
- package/dist/svelte/index.d.ts +8 -0
- package/dist/svelte/index.js +6 -0
- package/dist/vue.d.mts +32 -0
- package/dist/vue.mjs +26 -0
- package/package.json +88 -3
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,50 @@
|
|
|
1
|
-
#
|
|
1
|
+
# comark-table
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://npmx.dev/package/comark-table)
|
|
4
|
+
[](https://npmx.dev/package/comark-table)
|
|
5
|
+
[](https://npmx.dev/package/comark-table)
|
|
6
|
+
[](https://comark-ui.onmax.me/table/)
|
|
7
|
+
|
|
8
|
+
Tables from CSV, TSV, and JSON files or fences, read while parsing, with a Copy button that pastes as cells in a spreadsheet, a menu that copies it as Markdown or CSV, and headers that sort. A [Comark](https://comark.dev) plugin for HTML, Vue, React, and Svelte.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add comark comark-table
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { createHtmlRenderer } from "@comark/html";
|
|
16
|
+
import table, { components } from "comark-table/html";
|
|
17
|
+
|
|
18
|
+
const render = createHtmlRenderer({ plugins: [table({ root: "docs" })], components });
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
````md
|
|
22
|
+
::table{src="bench/results.csv" sort}
|
|
23
|
+
::
|
|
24
|
+
|
|
25
|
+
```csv
|
|
26
|
+
Benchmark,Before (ms),After (ms)
|
|
27
|
+
Parse,"1,204",980
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
::table
|
|
31
|
+
```json
|
|
32
|
+
[{ "package": "zoom", "kB": 3.1 }, { "package": "tabs", "kB": 2.4 }]
|
|
33
|
+
```
|
|
34
|
+
::
|
|
35
|
+
````
|
|
36
|
+
|
|
37
|
+
`src` reads a file relative to the document, under `root`, as comark-include does; pass `read` in a browser or an edge runtime. A ` ```csv ` or ` ```tsv ` fence becomes a table anywhere (`fences: false` keeps them as code); a ` ```json ` fence, an array of objects, becomes one inside `::table`, and so does a Markdown table. `sort` makes the headers sort the rows, and `copy=false` hides the Copy button and its menu. Number columns line up on the right.
|
|
38
|
+
|
|
39
|
+
The plugin adds every problem to `meta.issues`, so a host can reject a document before publishing it: a missing file, a path outside the root, a URL, an unknown format, JSON that isn't an array of objects, an unclosed quote, and rows of the wrong length. A fence it can't read stays code. Each issue names its table and says how to fix it.
|
|
40
|
+
|
|
41
|
+
## Renderers
|
|
42
|
+
|
|
43
|
+
| Renderer | Import |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `@comark/html` | `comark-table/html`: the script and styles are inlined once per document |
|
|
46
|
+
| `@comark/vue` | `comark-table/vue` |
|
|
47
|
+
| `@comark/react` | `comark-table/react` |
|
|
48
|
+
| `@comark/svelte` | `comark-table/svelte` |
|
|
49
|
+
|
|
50
|
+
Each entry exports the parse plugin as `default` and the renderer's `components`. The Vue, React, and Svelte components start the script when they mount; import `comark-table/style.css` with your styles so a server-rendered page doesn't shift. The styles read the shared `--comark-*` tokens and sit in a `comark` CSS layer, so any plain rule of your own wins.
|
|
@@ -0,0 +1,275 @@
|
|
|
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 table that scrolls sideways in place, with a Copy button and a menu of formats above it and headers that sort, on the shared
|
|
61
|
+
* `--comark-*` tokens. The buttons and sort arrows only show once the script runs. The rules sit in the `comark` layer,
|
|
62
|
+
* so any unlayered rule of the host's wins.
|
|
63
|
+
*/
|
|
64
|
+
const css = `@layer comark{${[
|
|
65
|
+
theme(".ctb"),
|
|
66
|
+
".ctb{margin:1.5rem 0}",
|
|
67
|
+
".ctb-scroll{overflow-x:auto}",
|
|
68
|
+
".ctb-scroll>table{margin:0}",
|
|
69
|
+
".ctb-bar{display:flex;align-items:center;justify-content:flex-end;gap:.25rem;margin-bottom:.375rem;font:500 .75rem/1rem var(--_comark-font)}",
|
|
70
|
+
"html:not([data-table-ready]) .ctb-bar{display:none}",
|
|
71
|
+
".ctb-copy{position:relative;display:inline-flex;border:1px solid var(--_comark-border);border-radius:calc(var(--_comark-radius) - 4px);background:var(--_comark-background)}",
|
|
72
|
+
".ctb-copy>button{display:inline-grid;place-items:center;height:1.625rem;padding:0;border:0;background:none;color:var(--_comark-muted);font:inherit;cursor:pointer;transition:background-color .15s,color .15s}",
|
|
73
|
+
".ctb-copy-main{width:1.75rem;border-radius:calc(var(--_comark-radius) - 5px) 0 0 calc(var(--_comark-radius) - 5px)}",
|
|
74
|
+
".ctb-copy>.ctb-copy-more{width:1.25rem;border-inline-start:1px solid var(--_comark-border);border-radius:0 calc(var(--_comark-radius) - 5px) calc(var(--_comark-radius) - 5px) 0}",
|
|
75
|
+
".ctb-copy-more svg{width:.75rem;height:.75rem}",
|
|
76
|
+
".ctb-copy-more[aria-expanded=true]{background:var(--_comark-accent);color:var(--_comark-foreground)}",
|
|
77
|
+
"@media (hover:hover){.ctb-copy>button:hover,.ctb-menu button:hover{background:var(--_comark-accent);color:var(--_comark-foreground)}}",
|
|
78
|
+
".ctb-copy-main svg+svg,.ctb-copy-main[data-state=done] svg:first-child{display:none}",
|
|
79
|
+
".ctb-copy-main[data-state=done] svg+svg{display:block}",
|
|
80
|
+
".ctb-menu{position:absolute;top:calc(100% + .25rem);right:-1px;z-index:10;display:grid;min-width:15rem;padding:.25rem;border:1px solid var(--_comark-border);border-radius:var(--_comark-radius);background:var(--_comark-background);box-shadow:0 4px 16px rgb(0 0 0/.12),0 1px 3px rgb(0 0 0/.08)}",
|
|
81
|
+
".ctb-menu[hidden]{display:none}",
|
|
82
|
+
".ctb-menu button{display:grid;grid-template-columns:.875rem 1fr;gap:.5rem;align-items:start;padding:.375rem .5rem;border:0;border-radius:calc(var(--_comark-radius) - 4px);background:none;color:var(--_comark-foreground);font:inherit;text-align:start;cursor:pointer}",
|
|
83
|
+
".ctb-menu button:focus-visible{outline:none;background:var(--_comark-accent)}",
|
|
84
|
+
".ctb-menu svg{margin-top:.125rem}",
|
|
85
|
+
".ctb-menu b{display:block;font-weight:500}",
|
|
86
|
+
".ctb-menu small{display:block;color:var(--_comark-muted);font-size:.75rem;font-weight:400;line-height:1rem}",
|
|
87
|
+
".ctb-status{margin-inline-end:.25rem;color:var(--_comark-muted)}",
|
|
88
|
+
".ctb-sort{all:unset}",
|
|
89
|
+
".ctb-sort{margin:0!important;padding:0!important;border:0!important;background:none!important;box-shadow:none!important;font:inherit!important;color:inherit!important;text-align:inherit!important}",
|
|
90
|
+
"html[data-table-ready] .ctb-sort{display:inline-flex;align-items:center;gap:.25rem;cursor:pointer}",
|
|
91
|
+
"html[data-table-ready] .ctb-sort::after{content:'↕';color:var(--_comark-muted);font-weight:400}",
|
|
92
|
+
"html[data-table-ready] [aria-sort='ascending']>.ctb-sort::after{content:'↑';color:inherit}",
|
|
93
|
+
"html[data-table-ready] [aria-sort='descending']>.ctb-sort::after{content:'↓';color:inherit}",
|
|
94
|
+
".ctb-copy>button:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--_comark-ring) 50%,transparent)}",
|
|
95
|
+
".ctb-sort:focus-visible{outline:none;border-radius:calc(var(--_comark-radius) - 4px);box-shadow:0 0 0 3px color-mix(in srgb,var(--_comark-ring) 50%,transparent)!important}",
|
|
96
|
+
"@media print{.ctb-bar{display:none}.ctb-sort::after{content:none!important}}"
|
|
97
|
+
].join("")}}`;
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/client.ts
|
|
100
|
+
const icon = (body) => `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${body}</svg>`;
|
|
101
|
+
const copyIcon = icon("<rect x=\"8\" y=\"8\" width=\"14\" height=\"14\" rx=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/>");
|
|
102
|
+
const doneIcon = icon("<path d=\"M20 6 9 17l-5-5\"/>");
|
|
103
|
+
const formats = [
|
|
104
|
+
[
|
|
105
|
+
"table",
|
|
106
|
+
"Table",
|
|
107
|
+
"Pastes as cells in a spreadsheet",
|
|
108
|
+
"<rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"/><path d=\"M3 9h18M3 15h18M9 3v18\"/>"
|
|
109
|
+
],
|
|
110
|
+
[
|
|
111
|
+
"markdown",
|
|
112
|
+
"Markdown",
|
|
113
|
+
"A table that renders on GitHub",
|
|
114
|
+
"<path d=\"M4 7v10M4 7l4 5 4-5v10M17 7v10M14 14l3 3 3-3\"/>"
|
|
115
|
+
],
|
|
116
|
+
[
|
|
117
|
+
"csv",
|
|
118
|
+
"CSV",
|
|
119
|
+
"Comma-separated text",
|
|
120
|
+
"<path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v5h6M16 13H8M16 17H8M10 9H8\"/>"
|
|
121
|
+
]
|
|
122
|
+
];
|
|
123
|
+
/**
|
|
124
|
+
* The bar above a table: a status line for screen readers, then one Copy button for the table and a menu of the other
|
|
125
|
+
* formats. Every renderer inlines this string.
|
|
126
|
+
*/
|
|
127
|
+
function bar() {
|
|
128
|
+
const items = formats.filter(([format]) => format !== "table").map(([format, name, hint, path]) => `<button type="button" role="menuitem" tabindex="-1" data-table-copy="${format}">${icon(path)}<span><b>${name}</b><small>${hint}</small></span></button>`).join("");
|
|
129
|
+
return `<span class="ctb-status" role="status"></span><div class="ctb-copy"><button type="button" class="ctb-copy-main" data-table-copy="table" aria-label="Copy table" title="Copy table">${copyIcon}${doneIcon}</button><button type="button" class="ctb-copy-more" aria-label="More ways to copy" title="More ways to copy" aria-haspopup="menu" aria-expanded="false">${icon("<path d=\"m6 9 6 6 6-6\"/>")}</button><div class="ctb-menu" role="menu" aria-label="Copy as" hidden>${items}</div></div>`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Starts the copy buttons and sortable headers of every `.ctb` table on the page and returns a function that stops
|
|
133
|
+
* them. Every caller shares one set of listeners, which stop with the last caller. The function is self-contained: the
|
|
134
|
+
* HTML renderer inlines its source as a script, so it must not use anything from outside its body.
|
|
135
|
+
*/
|
|
136
|
+
function start(css) {
|
|
137
|
+
const running = window.__comarkTable;
|
|
138
|
+
if (running) {
|
|
139
|
+
running.users++;
|
|
140
|
+
return running.stop;
|
|
141
|
+
}
|
|
142
|
+
const listeners = new AbortController();
|
|
143
|
+
if (!document.querySelector("style[data-comark-table],link[data-comark-table]")) {
|
|
144
|
+
const style = document.createElement("style");
|
|
145
|
+
style.setAttribute("data-comark-table", "");
|
|
146
|
+
style.textContent = css;
|
|
147
|
+
document.head.append(style);
|
|
148
|
+
}
|
|
149
|
+
document.documentElement.setAttribute("data-table-ready", "");
|
|
150
|
+
const cells = (table) => [...table.rows].map((row) => [...row.cells].map((cell) => (cell.textContent ?? "").trim()));
|
|
151
|
+
const csv = (rows) => rows.map((row) => row.map((cell) => /[",\n]/.test(cell) ? `"${cell.replaceAll("\"", "\"\"")}"` : cell).join(",")).join("\n");
|
|
152
|
+
const tsv = (rows) => rows.map((row) => row.map((cell) => cell.replace(/[\t\n]+/g, " ")).join(" ")).join("\n");
|
|
153
|
+
const markdown = ([header = [], ...body]) => [
|
|
154
|
+
header,
|
|
155
|
+
header.map(() => "---"),
|
|
156
|
+
...body
|
|
157
|
+
].map((row) => `| ${row.map((cell) => cell.replaceAll("|", "\\|").replaceAll("\n", " ")).join(" | ")} |`).join("\n");
|
|
158
|
+
const escape = (cell) => cell.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
159
|
+
const el = (tag, inner) => `<${tag}>${inner}</${tag}>`;
|
|
160
|
+
const tr = (line, tag) => el("tr", line.map((cell) => el(tag, escape(cell))).join(""));
|
|
161
|
+
const html = ([header = [], ...body]) => el("table", el("thead", tr(header, "th")) + el("tbody", body.map((line) => tr(line, "td")).join("")));
|
|
162
|
+
const named = (format) => format === "csv" ? "CSV" : format === "markdown" ? "Markdown" : format;
|
|
163
|
+
function copy(root, format) {
|
|
164
|
+
const table = root.querySelector("table");
|
|
165
|
+
const status = root.querySelector(".ctb-status");
|
|
166
|
+
const main = root.querySelector(".ctb-copy-main");
|
|
167
|
+
if (!table || !status) return;
|
|
168
|
+
const rows = cells(table);
|
|
169
|
+
const say = (message, state) => {
|
|
170
|
+
status.textContent = message;
|
|
171
|
+
main?.setAttribute("data-state", state);
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
if (status.textContent !== message) return;
|
|
174
|
+
status.textContent = "";
|
|
175
|
+
main?.removeAttribute("data-state");
|
|
176
|
+
}, 1500);
|
|
177
|
+
};
|
|
178
|
+
const clipboard = navigator.clipboard;
|
|
179
|
+
if (!clipboard) return say("Couldn't copy here", "failed");
|
|
180
|
+
const text = format === "csv" ? csv(rows) : format === "markdown" ? markdown(rows) : tsv(rows);
|
|
181
|
+
(format === "table" && "ClipboardItem" in window ? clipboard.write([new ClipboardItem({
|
|
182
|
+
"text/html": new Blob([html(rows)], { type: "text/html" }),
|
|
183
|
+
"text/plain": new Blob([text], { type: "text/plain" })
|
|
184
|
+
})]) : clipboard.writeText(text)).then(() => say(`Copied as ${named(format)}`, "done"), () => say("Couldn't copy here", "failed"));
|
|
185
|
+
}
|
|
186
|
+
function close(more, focus = false) {
|
|
187
|
+
const menu = more.nextElementSibling;
|
|
188
|
+
if (!(menu instanceof HTMLElement) || menu.hidden) return;
|
|
189
|
+
menu.hidden = true;
|
|
190
|
+
more.setAttribute("aria-expanded", "false");
|
|
191
|
+
if (focus) more.focus();
|
|
192
|
+
}
|
|
193
|
+
function open(more) {
|
|
194
|
+
const menu = more.nextElementSibling;
|
|
195
|
+
if (!(menu instanceof HTMLElement)) return;
|
|
196
|
+
menu.hidden = false;
|
|
197
|
+
more.setAttribute("aria-expanded", "true");
|
|
198
|
+
menu.querySelector("[role=menuitem]")?.focus();
|
|
199
|
+
}
|
|
200
|
+
const closeAll = (except) => {
|
|
201
|
+
for (const more of document.querySelectorAll(".ctb-copy-more[aria-expanded=true]")) if (more !== except) close(more);
|
|
202
|
+
};
|
|
203
|
+
const words = new Intl.Collator(void 0, {
|
|
204
|
+
numeric: true,
|
|
205
|
+
sensitivity: "base"
|
|
206
|
+
});
|
|
207
|
+
const number = (cell) => /^[+-]?(?:\d[\d,]*(?:\.\d+)?|\.\d+)/.test(cell) ? Number.parseFloat(cell.replaceAll(",", "")) : NaN;
|
|
208
|
+
function sort(header) {
|
|
209
|
+
const table = header.closest("table");
|
|
210
|
+
const body = table?.tBodies[0];
|
|
211
|
+
if (!table || !body) return;
|
|
212
|
+
const ascending = header.getAttribute("aria-sort") !== "ascending";
|
|
213
|
+
for (const other of table.querySelectorAll("th[aria-sort]")) other.removeAttribute("aria-sort");
|
|
214
|
+
header.setAttribute("aria-sort", ascending ? "ascending" : "descending");
|
|
215
|
+
const column = header.cellIndex;
|
|
216
|
+
const value = (row) => (row.cells[column]?.textContent ?? "").trim();
|
|
217
|
+
const rows = [...body.rows].sort((a, b) => {
|
|
218
|
+
const [left, right] = [value(a), value(b)];
|
|
219
|
+
const [x, y] = [number(left), number(right)];
|
|
220
|
+
const order = Number.isNaN(x) || Number.isNaN(y) ? words.compare(left, right) : x - y;
|
|
221
|
+
return ascending ? order : -order;
|
|
222
|
+
});
|
|
223
|
+
body.append(...rows);
|
|
224
|
+
}
|
|
225
|
+
document.addEventListener("click", (event) => {
|
|
226
|
+
const target = event.target instanceof Element ? event.target : null;
|
|
227
|
+
const more = target?.closest(".ctb>.ctb-bar .ctb-copy-more");
|
|
228
|
+
closeAll(more ?? void 0);
|
|
229
|
+
if (more) return more.getAttribute("aria-expanded") === "true" ? close(more) : open(more);
|
|
230
|
+
const button = target?.closest(".ctb>.ctb-bar [data-table-copy]");
|
|
231
|
+
const root = button?.closest(".ctb");
|
|
232
|
+
if (button && root) {
|
|
233
|
+
copy(root, button.getAttribute("data-table-copy") ?? "table");
|
|
234
|
+
const owner = button.closest(".ctb-menu")?.previousElementSibling;
|
|
235
|
+
if (owner instanceof HTMLElement) owner.focus();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const header = target?.closest(".ctb-sort")?.closest("th");
|
|
239
|
+
if (header) sort(header);
|
|
240
|
+
}, { signal: listeners.signal });
|
|
241
|
+
document.addEventListener("keydown", (event) => {
|
|
242
|
+
const target = event.target instanceof HTMLElement ? event.target : null;
|
|
243
|
+
if (target?.matches(".ctb-copy-more") && event.key === "ArrowDown") {
|
|
244
|
+
event.preventDefault();
|
|
245
|
+
return open(target);
|
|
246
|
+
}
|
|
247
|
+
const menu = target?.closest(".ctb-menu");
|
|
248
|
+
const more = menu?.previousElementSibling;
|
|
249
|
+
if (!target || !menu || !(more instanceof HTMLElement)) return;
|
|
250
|
+
const items = [...menu.querySelectorAll("[role=menuitem]")];
|
|
251
|
+
const at = items.indexOf(target);
|
|
252
|
+
const { key } = event;
|
|
253
|
+
const step = key === "ArrowDown" ? at + 1 : key === "ArrowUp" ? at - 1 : key === "Home" ? 0 : key === "End" ? items.length - 1 : void 0;
|
|
254
|
+
if (step !== void 0) {
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
items[(step + items.length) % items.length]?.focus();
|
|
257
|
+
} else if (event.key === "Escape") {
|
|
258
|
+
event.preventDefault();
|
|
259
|
+
close(more, true);
|
|
260
|
+
} else if (event.key === "Tab") close(more);
|
|
261
|
+
}, { signal: listeners.signal });
|
|
262
|
+
const handle = {
|
|
263
|
+
users: 1,
|
|
264
|
+
stop: () => {
|
|
265
|
+
if (--handle.users > 0) return;
|
|
266
|
+
listeners.abort();
|
|
267
|
+
document.documentElement.removeAttribute("data-table-ready");
|
|
268
|
+
delete window.__comarkTable;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
window.__comarkTable = handle;
|
|
272
|
+
return handle.stop;
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
export { start as n, css as r, bar as t };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//#region src/styles.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A table that scrolls sideways in place, with a Copy button and a menu of formats above it and headers that sort, on the shared
|
|
4
|
+
* `--comark-*` tokens. The buttons and sort arrows only show once the script runs. The rules sit in the `comark` layer,
|
|
5
|
+
* 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
|
+
declare global {
|
|
11
|
+
interface Window {
|
|
12
|
+
__comarkTable?: {
|
|
13
|
+
users: number;
|
|
14
|
+
stop: () => void;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The bar above a table: a status line for screen readers, then one Copy button for the table and a menu of the other
|
|
20
|
+
* formats. Every renderer inlines this string.
|
|
21
|
+
*/
|
|
22
|
+
export declare function bar(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Starts the copy buttons and sortable headers of every `.ctb` table on the page and returns a function that stops
|
|
25
|
+
* them. Every caller shares one set of listeners, which stop with the last caller. The function is self-contained: the
|
|
26
|
+
* HTML renderer inlines its source as a script, so it must not use anything from outside its body.
|
|
27
|
+
*/
|
|
28
|
+
export declare function start(css: string): () => void;
|
|
29
|
+
//#endregion
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { t as bar, r as css, n as start } from "./client-C5bRu9NG.mjs";
|
package/dist/html.d.mts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import _default, { TABLE_TAG, TableAttributes, TableMeta, TableOptions, TableReader } from "./index.mjs";
|
|
2
|
+
import { NodeHandler } from "comark";
|
|
3
|
+
//#region src/html.d.ts
|
|
4
|
+
/** The HTML renderer for tables. Pass it as `components`; it inlines the script and styles once per document. */
|
|
5
|
+
export declare const components: Record<"data-table", NodeHandler>;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { TABLE_TAG, type TableAttributes, type TableMeta, type TableOptions, type TableReader, _default as default };
|
package/dist/html.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import src_default, { TABLE_TAG } from "./index.mjs";
|
|
2
|
+
import { n as start, r as css, t as bar } from "./client-C5bRu9NG.mjs";
|
|
3
|
+
//#region src/html.ts
|
|
4
|
+
const started = /* @__PURE__ */ new WeakSet();
|
|
5
|
+
/** The table script as one inline `<script>`, once per render. */
|
|
6
|
+
function runtime(state) {
|
|
7
|
+
if (started.has(state.context)) return "";
|
|
8
|
+
started.add(state.context);
|
|
9
|
+
return `<style data-comark-table>${css}</style><script>(${start.toString()})("")<\/script>`;
|
|
10
|
+
}
|
|
11
|
+
const on = (value) => value === true || value === "true";
|
|
12
|
+
const table = async ([, attrs, ...children], state) => `<div class="ctb">${on(attrs.copy) ? `<div class="ctb-bar">${bar()}</div>` : ""}<div class="ctb-scroll">${await state.flow([
|
|
13
|
+
"div",
|
|
14
|
+
{},
|
|
15
|
+
...children
|
|
16
|
+
], state)}</div></div>${runtime(state)}`;
|
|
17
|
+
/** The HTML renderer for tables. Pass it as `components`; it inlines the script and styles once per document. */
|
|
18
|
+
const components = { "data-table": table };
|
|
19
|
+
//#endregion
|
|
20
|
+
export { TABLE_TAG, components, src_default as default };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
/** Reads a file by its POSIX path relative to `root`, such as `bench/results.csv`. Returns undefined when it doesn't exist. */
|
|
3
|
+
export type TableReader = (path: string) => string | undefined | Promise<string | undefined>;
|
|
4
|
+
export interface TableOptions {
|
|
5
|
+
/** The folder a `src` can't leave, and where a `src` that starts with `/` starts. Default `"."`, the working directory. */
|
|
6
|
+
root?: string;
|
|
7
|
+
/** The document's folder relative to `root`, where relative paths start. Default `""`, the root itself. */
|
|
8
|
+
base?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Reads a file under `root`. Default reads from disk and refuses symlinks that lead out of `root`, so browsers and
|
|
11
|
+
* edge runtimes pass their own, as with comark-include.
|
|
12
|
+
*/
|
|
13
|
+
read?: TableReader;
|
|
14
|
+
/** Turn ` ```csv ` and ` ```tsv ` fences outside `::table` into tables. Default `true`. */
|
|
15
|
+
fences?: boolean;
|
|
16
|
+
}
|
|
17
|
+
export type TableMeta = {
|
|
18
|
+
/** One readable sentence per problem with a `::table` or a data fence. Other plugins add theirs too. */
|
|
19
|
+
issues: string[];
|
|
20
|
+
/** The comark-linter rule of each issue, by its sentence, so the linter reports it under its own code. */
|
|
21
|
+
issueRules: Record<string, string>;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* What a `::table` or a data fence becomes: a `data-table` node around a plain `table`. `sort` makes the headers
|
|
25
|
+
* buttons, and `copy` shows the Copy button and its menu of formats.
|
|
26
|
+
*/
|
|
27
|
+
export type TableAttributes = {
|
|
28
|
+
sort?: boolean;
|
|
29
|
+
copy?: boolean;
|
|
30
|
+
};
|
|
31
|
+
/** The tag the plugin's tables carry, since `::table` shares its name with every Markdown table. */
|
|
32
|
+
export declare const TABLE_TAG = "data-table";
|
|
33
|
+
/**
|
|
34
|
+
* Turns `::table{src="bench.csv"}`, a `::table` around a csv, tsv, or json fence or a Markdown table, and every bare
|
|
35
|
+
* ` ```csv ` or ` ```tsv ` fence into a table while parsing, with buttons that copy it as CSV or Markdown and, with
|
|
36
|
+
* `sort`, headers that sort it. Problems are added to `meta.issues`.
|
|
37
|
+
*/
|
|
38
|
+
declare const _default: import("comark").ComarkPluginFactory<TableOptions | undefined, TableMeta, {}>;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { _default as default };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import { defineComarkPlugin } from "comark";
|
|
2
|
+
import { resolveAttributes } from "comark/render";
|
|
3
|
+
//#region src/index.ts
|
|
4
|
+
/** The tag the plugin's tables carry, since `::table` shares its name with every Markdown table. */
|
|
5
|
+
const TABLE_TAG = "data-table";
|
|
6
|
+
const FORMATS = /* @__PURE__ */ new Set([
|
|
7
|
+
"csv",
|
|
8
|
+
"tsv",
|
|
9
|
+
"json"
|
|
10
|
+
]);
|
|
11
|
+
const NUMBER = /^[+-]?(?:\d[\d,]*(?:\.\d+)?|\.\d+)(?:\s*\S{1,4})?$/;
|
|
12
|
+
const isElement = (node) => Array.isArray(node) && node[0] !== null;
|
|
13
|
+
const childrenOf = ([, , ...rest]) => rest;
|
|
14
|
+
const isContent = (node) => Array.isArray(node) ? node[0] !== null : node.trim() !== "";
|
|
15
|
+
const textOf = (node) => !Array.isArray(node) ? node : isElement(node) ? childrenOf(node).map(textOf).join("") : "";
|
|
16
|
+
const text = (value) => value === void 0 || value === null || value instanceof Object ? "" : String(value).trim();
|
|
17
|
+
const on = (value) => value !== void 0 && value !== false && text(value) !== "false";
|
|
18
|
+
const errorText = (error) => error instanceof Error ? error.message : String(error);
|
|
19
|
+
const isMarkdownTable = (node) => node[0] === "table" && childrenOf(node).some((child) => isElement(child) && child[0] === "thead");
|
|
20
|
+
const PROPS = [
|
|
21
|
+
"src",
|
|
22
|
+
"format",
|
|
23
|
+
"sort",
|
|
24
|
+
"copy"
|
|
25
|
+
];
|
|
26
|
+
const isBlock = (node) => node[0] === "table" && (!isMarkdownTable(node) || Object.keys(node[1]).some((key) => PROPS.includes(key.replace(/^:/, ""))));
|
|
27
|
+
const TONES = {
|
|
28
|
+
success: "var(--ui-success,#16a34a)",
|
|
29
|
+
warning: "var(--ui-warning,#ca8a04)",
|
|
30
|
+
error: "var(--ui-error,var(--destructive,#dc2626))",
|
|
31
|
+
info: "var(--ui-info,#2563eb)"
|
|
32
|
+
};
|
|
33
|
+
const isKnownTone = (tone) => Object.hasOwn(TONES, tone);
|
|
34
|
+
const TONED = /^\[(.*)\]\{\s*tone=(?:"([^"]*)"|'([^']*)'|([^\s}]*))\s*\}$/;
|
|
35
|
+
const isTone = (node) => isElement(node) && node[0] === "span" && Object.hasOwn(node[1], "tone");
|
|
36
|
+
const cellNode = (cell) => {
|
|
37
|
+
const toned = TONED.exec(cell);
|
|
38
|
+
return toned ? [
|
|
39
|
+
"span",
|
|
40
|
+
{ tone: toned[2] ?? toned[3] ?? toned[4] },
|
|
41
|
+
toned[1] ?? ""
|
|
42
|
+
] : cell;
|
|
43
|
+
};
|
|
44
|
+
const cellText = (cell) => TONED.exec(cell)?.[1] ?? cell;
|
|
45
|
+
/** RFC 4180: quoted cells may hold the separator, line breaks, and `""` for a quote. */
|
|
46
|
+
function delimited(source, separator) {
|
|
47
|
+
const rows = [];
|
|
48
|
+
let row = [];
|
|
49
|
+
let cell = "";
|
|
50
|
+
let quoted = false;
|
|
51
|
+
for (let index = 0; index < source.length; index++) {
|
|
52
|
+
const char = source[index];
|
|
53
|
+
if (quoted) {
|
|
54
|
+
if (char !== "\"") cell += char;
|
|
55
|
+
else if (source[index + 1] === "\"") {
|
|
56
|
+
cell += "\"";
|
|
57
|
+
index++;
|
|
58
|
+
} else quoted = false;
|
|
59
|
+
} else if (char === "\"" && !cell.trim()) {
|
|
60
|
+
quoted = true;
|
|
61
|
+
cell = "";
|
|
62
|
+
} else if (char === separator) {
|
|
63
|
+
row.push(cell.trim());
|
|
64
|
+
cell = "";
|
|
65
|
+
} else if (char === "\n") {
|
|
66
|
+
row.push(cell.trim());
|
|
67
|
+
rows.push(row);
|
|
68
|
+
row = [];
|
|
69
|
+
cell = "";
|
|
70
|
+
} else if (char !== "\r") cell += char;
|
|
71
|
+
}
|
|
72
|
+
if (quoted) return void 0;
|
|
73
|
+
row.push(cell.trim());
|
|
74
|
+
rows.push(row);
|
|
75
|
+
return rows.filter((cells) => cells.some(Boolean));
|
|
76
|
+
}
|
|
77
|
+
/** TSV is tab-separated text without CSV's quote syntax; quotes are data. */
|
|
78
|
+
function tsv(source) {
|
|
79
|
+
return source.split("\n").map((line) => line.split(" ").map((cell) => cell.trim())).filter((cells) => cells.some(Boolean));
|
|
80
|
+
}
|
|
81
|
+
const value = (item) => item === null || item === void 0 ? "" : item instanceof Object ? JSON.stringify(item) : text(item);
|
|
82
|
+
/** The header and rows of a file or a fence, or why there are none. */
|
|
83
|
+
function rowsOf(source, format) {
|
|
84
|
+
if (format === "json") {
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = JSON.parse(source);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return { issue: {
|
|
90
|
+
rule: "comark/table/invalid-json",
|
|
91
|
+
message: `its JSON doesn't parse: ${errorText(error)}. Fix the JSON.`
|
|
92
|
+
} };
|
|
93
|
+
}
|
|
94
|
+
if (!Array.isArray(parsed) || !parsed.every((item) => item instanceof Object && !Array.isArray(item))) return { issue: {
|
|
95
|
+
rule: "comark/table/json-not-rows",
|
|
96
|
+
message: "its JSON must be an array of objects, one per row, such as `[{\"name\": \"a\", \"size\": 1}]`."
|
|
97
|
+
} };
|
|
98
|
+
const items = parsed;
|
|
99
|
+
const columns = [...new Set(items.flatMap((item) => Object.keys(item)))];
|
|
100
|
+
return { rows: [columns, ...items.map((item) => columns.map((column) => value(item[column])))] };
|
|
101
|
+
}
|
|
102
|
+
const normalized = source.replace(/\r\n?/g, "\n");
|
|
103
|
+
const rows = format === "tsv" ? tsv(normalized) : delimited(normalized, ",");
|
|
104
|
+
return rows ? { rows } : { issue: {
|
|
105
|
+
rule: "comark/table/unclosed-quote",
|
|
106
|
+
message: "a quoted cell never ends. Close it with `\"`, and write a quote inside one as `\"\"`."
|
|
107
|
+
} };
|
|
108
|
+
}
|
|
109
|
+
function build(rows, sort, name, issues) {
|
|
110
|
+
const [header = [], ...body] = rows;
|
|
111
|
+
if (!header.length || !body.length) {
|
|
112
|
+
issues.push({
|
|
113
|
+
rule: "comark/table/no-rows",
|
|
114
|
+
message: `${name} has no rows. Give it a header row and at least one row of data.`
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const uneven = body.findIndex((row) => row.length !== header.length);
|
|
119
|
+
if (uneven !== -1) issues.push({
|
|
120
|
+
rule: "comark/table/uneven-row",
|
|
121
|
+
message: `${name}: row ${uneven + 1} has ${body[uneven]?.length} cells, but the header has ${header.length}. Give every row one cell per column.`
|
|
122
|
+
});
|
|
123
|
+
const numeric = header.map((_, column) => body.every((row) => !row[column] || NUMBER.test(cellText(row[column]))));
|
|
124
|
+
const style = (column) => numeric[column] ? { style: "text-align:right" } : {};
|
|
125
|
+
const table = [
|
|
126
|
+
"table",
|
|
127
|
+
{},
|
|
128
|
+
[
|
|
129
|
+
"thead",
|
|
130
|
+
{},
|
|
131
|
+
[
|
|
132
|
+
"tr",
|
|
133
|
+
{},
|
|
134
|
+
...header.map((label, column) => [
|
|
135
|
+
"th",
|
|
136
|
+
style(column),
|
|
137
|
+
cellNode(label)
|
|
138
|
+
])
|
|
139
|
+
]
|
|
140
|
+
],
|
|
141
|
+
[
|
|
142
|
+
"tbody",
|
|
143
|
+
{},
|
|
144
|
+
...body.map((row) => [
|
|
145
|
+
"tr",
|
|
146
|
+
{},
|
|
147
|
+
...header.map((_, column) => [
|
|
148
|
+
"td",
|
|
149
|
+
style(column),
|
|
150
|
+
cellNode(row[column] ?? "")
|
|
151
|
+
])
|
|
152
|
+
])
|
|
153
|
+
]
|
|
154
|
+
];
|
|
155
|
+
color(table, name, issues);
|
|
156
|
+
if (sort) sortable(table);
|
|
157
|
+
return table;
|
|
158
|
+
}
|
|
159
|
+
/** Takes `tone` off every span under `node`, unwrapping a span it leaves bare, and returns those spans. */
|
|
160
|
+
function takeTones(node) {
|
|
161
|
+
const spans = [];
|
|
162
|
+
const children = childrenOf(node).flatMap((child) => {
|
|
163
|
+
if (!isElement(child)) return [child];
|
|
164
|
+
spans.push(...takeTones(child));
|
|
165
|
+
if (!isTone(child)) return [child];
|
|
166
|
+
spans.push(child);
|
|
167
|
+
const { tone: _, ...rest } = child[1];
|
|
168
|
+
return Object.keys(rest).length ? [[
|
|
169
|
+
child[0],
|
|
170
|
+
rest,
|
|
171
|
+
...childrenOf(child)
|
|
172
|
+
]] : childrenOf(child);
|
|
173
|
+
});
|
|
174
|
+
node.splice(2, node.length - 2, ...children);
|
|
175
|
+
return spans;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Tints each cell that is one `[text]{tone="error"}` span, and each row whose first cell is, so the span itself never
|
|
179
|
+
* renders. The tint goes on every cell rather than the row, since a page's striped rows paint their cells.
|
|
180
|
+
*/
|
|
181
|
+
function color(table, name, issues) {
|
|
182
|
+
let index = 0;
|
|
183
|
+
for (const section of childrenOf(table)) {
|
|
184
|
+
if (!isElement(section) || section[0] !== "thead" && section[0] !== "tbody") continue;
|
|
185
|
+
for (const row of childrenOf(section)) {
|
|
186
|
+
if (!isElement(row) || row[0] !== "tr") continue;
|
|
187
|
+
const where = section[0] === "thead" ? `${name}, header row` : `${name}, row ${++index}`;
|
|
188
|
+
const cells = childrenOf(row).filter((cell) => isElement(cell) && (cell[0] === "td" || cell[0] === "th"));
|
|
189
|
+
const tones = cells.map((cell) => {
|
|
190
|
+
const content = childrenOf(cell).filter(isContent);
|
|
191
|
+
const [only] = content;
|
|
192
|
+
const filling = content.length === 1 && only && isTone(only) ? only : void 0;
|
|
193
|
+
let found;
|
|
194
|
+
let invalid = false;
|
|
195
|
+
for (const span of takeTones(cell)) {
|
|
196
|
+
const tone = text(span[1].tone);
|
|
197
|
+
if (span !== filling) {
|
|
198
|
+
invalid = true;
|
|
199
|
+
issues.push({
|
|
200
|
+
rule: "comark/table/partial-tone",
|
|
201
|
+
message: `${where}: the \`tone\` span "[${textOf(span)}]" doesn't fill its cell, so the cell isn't colored. Put the whole cell in the brackets, as \`[${textOf(cell)}]{tone="${tone}"}\`, or drop \`{tone}\`.`
|
|
202
|
+
});
|
|
203
|
+
} else if (!isKnownTone(tone)) {
|
|
204
|
+
invalid = true;
|
|
205
|
+
issues.push({
|
|
206
|
+
rule: "comark/table/unknown-tone",
|
|
207
|
+
message: `${where}: "${tone}" isn't a tone, so the cell isn't colored. Use \`success\`, \`warning\`, \`error\`, or \`info\`.`
|
|
208
|
+
});
|
|
209
|
+
} else found = tone;
|
|
210
|
+
}
|
|
211
|
+
return invalid ? null : found;
|
|
212
|
+
});
|
|
213
|
+
cells.forEach((cell, column) => {
|
|
214
|
+
const tone = tones[column] === void 0 ? tones[0] : tones[column];
|
|
215
|
+
if (!tone) return;
|
|
216
|
+
const tint = `background-color:color-mix(in srgb,${TONES[tone]} 20%,transparent)`;
|
|
217
|
+
cell[1] = {
|
|
218
|
+
...cell[1],
|
|
219
|
+
style: [text(cell[1].style), tint].filter(Boolean).join(";")
|
|
220
|
+
};
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/** Makes a Markdown table's headers sort buttons. */
|
|
226
|
+
function sortable(table) {
|
|
227
|
+
for (const section of childrenOf(table)) if (isElement(section) && section[0] === "thead") {
|
|
228
|
+
for (const row of childrenOf(section)) if (isElement(row)) {
|
|
229
|
+
for (const th of childrenOf(row)) if (isElement(th) && th[0] === "th") th.splice(2, th.length - 2, [
|
|
230
|
+
"button",
|
|
231
|
+
{
|
|
232
|
+
type: "button",
|
|
233
|
+
class: "ctb-sort"
|
|
234
|
+
},
|
|
235
|
+
...childrenOf(th)
|
|
236
|
+
]);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
function resolvePath(folder, src) {
|
|
241
|
+
const parts = folder ? folder.split("/") : [];
|
|
242
|
+
for (const part of src.split(/[/\\]/)) {
|
|
243
|
+
if (part === "" || part === ".") continue;
|
|
244
|
+
if (part !== "..") parts.push(part);
|
|
245
|
+
else if (parts.pop() === void 0) return void 0;
|
|
246
|
+
}
|
|
247
|
+
return parts.join("/");
|
|
248
|
+
}
|
|
249
|
+
var OutsideRoot = class extends Error {};
|
|
250
|
+
const isMissing = (error) => error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
|
|
251
|
+
function diskReader(root) {
|
|
252
|
+
const folder = (root || ".").replace(/[/\\]+$/, "");
|
|
253
|
+
return async (path) => {
|
|
254
|
+
const { readFile, realpath } = await import("node:fs/promises");
|
|
255
|
+
const paths = await import("node:path");
|
|
256
|
+
try {
|
|
257
|
+
const [base, target] = await Promise.all([realpath(folder), realpath(`${folder}/${path}`)]);
|
|
258
|
+
const inside = paths.relative(base, target);
|
|
259
|
+
if (inside === ".." || inside.startsWith(`..${paths.sep}`) || paths.isAbsolute(inside)) throw new OutsideRoot();
|
|
260
|
+
return await readFile(target, "utf8");
|
|
261
|
+
} catch (error) {
|
|
262
|
+
if (isMissing(error)) return void 0;
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
async function fromFile(src, name, run) {
|
|
268
|
+
const fail = (rule, issue) => void run.issues.push({
|
|
269
|
+
rule,
|
|
270
|
+
message: `${name}: ${issue}`
|
|
271
|
+
});
|
|
272
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(src)) return fail("comark/table/remote-src", "`src` must be a local file; a table can't read a URL.");
|
|
273
|
+
const path = resolvePath(src.startsWith("/") ? "" : run.base, src);
|
|
274
|
+
const leaves = "the path leaves the root. Move the file under the root or widen `root`.";
|
|
275
|
+
if (path === void 0) return fail("comark/table/path-outside-root", leaves);
|
|
276
|
+
let source;
|
|
277
|
+
try {
|
|
278
|
+
source = await run.read(path);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (error instanceof OutsideRoot) return fail("comark/table/path-outside-root", leaves);
|
|
281
|
+
return fail("comark/table/read-failed", `reading "${path}" failed: ${errorText(error)}. Check the file or the \`read\` option.`);
|
|
282
|
+
}
|
|
283
|
+
if (source === void 0) return fail("comark/table/file-not-found", `file not found. It resolved to "${path}" under the root; check the path or \`base\`.`);
|
|
284
|
+
return {
|
|
285
|
+
source,
|
|
286
|
+
format: path.slice(path.lastIndexOf(".") + 1).toLowerCase()
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
async function block(node, number, data, run) {
|
|
290
|
+
const attrs = resolveAttributes(node[1], data, { parseJson: true });
|
|
291
|
+
const src = text(attrs.src);
|
|
292
|
+
const name = `\`::table\` ${number}${src ? ` ("${src}")` : ""}`;
|
|
293
|
+
for (const key of Object.keys(node[1])) {
|
|
294
|
+
const prop = key.replace(/^:/, "");
|
|
295
|
+
if (!PROPS.includes(prop) && prop !== "id" && prop !== "class") run.issues.push({
|
|
296
|
+
rule: "comark/table/unknown-prop",
|
|
297
|
+
message: `${name}: \`${prop}\` isn't a prop of \`::table\`, so it's ignored. Use \`src\`, \`format\`, \`sort\`, or \`copy\`.`
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const sort = on(attrs.sort);
|
|
301
|
+
const result = (table) => table ? [
|
|
302
|
+
TABLE_TAG,
|
|
303
|
+
{
|
|
304
|
+
sort,
|
|
305
|
+
copy: attrs.copy === void 0 || on(attrs.copy)
|
|
306
|
+
},
|
|
307
|
+
table
|
|
308
|
+
] : ["div", {}];
|
|
309
|
+
if (!src && isMarkdownTable(node)) {
|
|
310
|
+
const inner = [
|
|
311
|
+
"table",
|
|
312
|
+
{},
|
|
313
|
+
...childrenOf(node)
|
|
314
|
+
];
|
|
315
|
+
if (sort) sortable(inner);
|
|
316
|
+
return result(inner);
|
|
317
|
+
}
|
|
318
|
+
const content = childrenOf(node).filter(isContent);
|
|
319
|
+
if (src && content.length) run.issues.push({
|
|
320
|
+
rule: "comark/table/content-dropped",
|
|
321
|
+
message: `${name} drops what's inside it, since it reads \`src\`. Use one or the other.`
|
|
322
|
+
});
|
|
323
|
+
let source;
|
|
324
|
+
let format = text(attrs.format).toLowerCase();
|
|
325
|
+
if (src) {
|
|
326
|
+
const file = await fromFile(src, name, run);
|
|
327
|
+
if (!file) return result(void 0);
|
|
328
|
+
source = file.source;
|
|
329
|
+
format ||= file.format;
|
|
330
|
+
} else {
|
|
331
|
+
const [inner] = content;
|
|
332
|
+
const fence = content.length === 1 && isElement(inner) && inner[0] === "pre" ? inner : void 0;
|
|
333
|
+
if (!fence) {
|
|
334
|
+
run.issues.push({
|
|
335
|
+
rule: "comark/table/empty-block",
|
|
336
|
+
message: `${name} has nothing to show. Give it \`src="data.csv"\`, or put one Markdown table or one csv, tsv, or json fence inside it.`
|
|
337
|
+
});
|
|
338
|
+
return result(void 0);
|
|
339
|
+
}
|
|
340
|
+
source = textOf(fence);
|
|
341
|
+
format ||= text(fence[1].language).toLowerCase();
|
|
342
|
+
}
|
|
343
|
+
if (!FORMATS.has(format)) {
|
|
344
|
+
run.issues.push({
|
|
345
|
+
rule: "comark/table/unknown-format",
|
|
346
|
+
message: `${name}: can't read ${format ? `"${format}"` : "data without a format"}. Use a .csv, .tsv, or .json file or fence, or set \`format\`.`
|
|
347
|
+
});
|
|
348
|
+
return result(void 0);
|
|
349
|
+
}
|
|
350
|
+
const read = rowsOf(source, format);
|
|
351
|
+
if ("issue" in read) {
|
|
352
|
+
run.issues.push({
|
|
353
|
+
rule: read.issue.rule,
|
|
354
|
+
message: `${name}: ${read.issue.message}`
|
|
355
|
+
});
|
|
356
|
+
return result(void 0);
|
|
357
|
+
}
|
|
358
|
+
return result(build(read.rows, sort, name, run.issues));
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Turns `::table{src="bench.csv"}`, a `::table` around a csv, tsv, or json fence or a Markdown table, and every bare
|
|
362
|
+
* ` ```csv ` or ` ```tsv ` fence into a table while parsing, with buttons that copy it as CSV or Markdown and, with
|
|
363
|
+
* `sort`, headers that sort it. Problems are added to `meta.issues`.
|
|
364
|
+
*/
|
|
365
|
+
var src_default = defineComarkPlugin((options = {}) => ({
|
|
366
|
+
name: "comark-table",
|
|
367
|
+
async post(state) {
|
|
368
|
+
const { tree } = state;
|
|
369
|
+
const data = {
|
|
370
|
+
frontmatter: tree.frontmatter,
|
|
371
|
+
meta: tree.meta,
|
|
372
|
+
data: {},
|
|
373
|
+
props: {}
|
|
374
|
+
};
|
|
375
|
+
const base = resolvePath("", options.base ?? "");
|
|
376
|
+
const run = {
|
|
377
|
+
read: options.read ?? diskReader(options.root ?? "."),
|
|
378
|
+
base: base ?? "",
|
|
379
|
+
issues: []
|
|
380
|
+
};
|
|
381
|
+
if (base === void 0) run.issues.push({
|
|
382
|
+
rule: "comark/table/base-outside-root",
|
|
383
|
+
message: `table: \`base\` "${options.base ?? ""}" leaves the root. Set it to the document's folder under \`root\`.`
|
|
384
|
+
});
|
|
385
|
+
let tables = 0;
|
|
386
|
+
const tint = (nodes) => {
|
|
387
|
+
for (const node of nodes) {
|
|
388
|
+
if (!isElement(node) || node[0] === "pre" || node[0] === "code") continue;
|
|
389
|
+
if (isMarkdownTable(node)) color(node, `Markdown table ${++tables}`, run.issues);
|
|
390
|
+
else tint(childrenOf(node));
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
tint(tree.nodes);
|
|
394
|
+
const count = {
|
|
395
|
+
block: 0,
|
|
396
|
+
fence: 0
|
|
397
|
+
};
|
|
398
|
+
const walk = async (nodes, start) => {
|
|
399
|
+
for (let index = start; index < nodes.length; index++) {
|
|
400
|
+
const node = nodes[index];
|
|
401
|
+
if (!node || !isElement(node)) continue;
|
|
402
|
+
const [tag, attrs] = node;
|
|
403
|
+
const language = text(attrs.language).toLowerCase();
|
|
404
|
+
if (isBlock(node)) nodes[index] = await block(node, ++count.block, data, run);
|
|
405
|
+
else if (tag === "pre" && options.fences !== false && (language === "csv" || language === "tsv")) {
|
|
406
|
+
const name = `The \`\`\`${language} fence ${++count.fence}`;
|
|
407
|
+
const read = rowsOf(textOf(node), language);
|
|
408
|
+
if ("issue" in read) run.issues.push({
|
|
409
|
+
rule: read.issue.rule,
|
|
410
|
+
message: `${name}: ${read.issue.message}`
|
|
411
|
+
});
|
|
412
|
+
const table = "issue" in read ? void 0 : build(read.rows, false, name, run.issues);
|
|
413
|
+
if (table) nodes[index] = [
|
|
414
|
+
TABLE_TAG,
|
|
415
|
+
{
|
|
416
|
+
sort: false,
|
|
417
|
+
copy: true
|
|
418
|
+
},
|
|
419
|
+
table
|
|
420
|
+
];
|
|
421
|
+
} else if (tag !== "pre" && tag !== "code") await walk(node, 2);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
await walk(tree.nodes, 0);
|
|
425
|
+
tree.meta.issues = [...tree.meta.issues ?? [], ...run.issues.map((issue) => issue.message)];
|
|
426
|
+
tree.meta.issueRules = {
|
|
427
|
+
...tree.meta.issueRules,
|
|
428
|
+
...Object.fromEntries(run.issues.map((issue) => [issue.message, issue.rule]))
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
}));
|
|
432
|
+
//#endregion
|
|
433
|
+
export { TABLE_TAG, src_default as default };
|
package/dist/react.d.mts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import _default, { TABLE_TAG, TableAttributes, TableMeta, TableOptions, TableReader } from "./index.mjs";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/react.d.ts
|
|
4
|
+
/** Tables for `@comark/react`. */
|
|
5
|
+
export declare function DataTable({ copy, children }: TableAttributes & {
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}): import("react").DetailedReactHTMLElement<{
|
|
8
|
+
className: string;
|
|
9
|
+
}, HTMLElement>;
|
|
10
|
+
/** Pass these as `components` to `@comark/react`. */
|
|
11
|
+
export declare const components: {
|
|
12
|
+
DataTable: typeof DataTable;
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { TABLE_TAG, type TableAttributes, type TableMeta, type TableOptions, type TableReader, _default as default };
|
package/dist/react.mjs
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import src_default, { TABLE_TAG } from "./index.mjs";
|
|
2
|
+
import { n as start, r as css, t as bar } from "./client-C5bRu9NG.mjs";
|
|
3
|
+
import { createElement, useEffect } from "react";
|
|
4
|
+
//#region src/react.ts
|
|
5
|
+
/** Tables for `@comark/react`. */
|
|
6
|
+
function DataTable({ copy, children }) {
|
|
7
|
+
useEffect(() => start(css), []);
|
|
8
|
+
return createElement("div", { className: "ctb" }, copy ? createElement("div", {
|
|
9
|
+
className: "ctb-bar",
|
|
10
|
+
dangerouslySetInnerHTML: { __html: bar() }
|
|
11
|
+
}) : null, createElement("div", { className: "ctb-scroll" }, children));
|
|
12
|
+
}
|
|
13
|
+
/** Pass these as `components` to `@comark/react`. */
|
|
14
|
+
const components = { DataTable };
|
|
15
|
+
//#endregion
|
|
16
|
+
export { DataTable, TABLE_TAG, components, src_default as default };
|
package/dist/style.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@layer comark{:where(.ctb){--_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(.ctb){--_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)))}}.ctb{margin:1.5rem 0}.ctb-scroll{overflow-x:auto}.ctb-scroll>table{margin:0}.ctb-bar{display:flex;align-items:center;justify-content:flex-end;gap:.25rem;margin-bottom:.375rem;font:500 .75rem/1rem var(--_comark-font)}html:not([data-table-ready]) .ctb-bar{display:none}.ctb-copy{position:relative;display:inline-flex;border:1px solid var(--_comark-border);border-radius:calc(var(--_comark-radius) - 4px);background:var(--_comark-background)}.ctb-copy>button{display:inline-grid;place-items:center;height:1.625rem;padding:0;border:0;background:none;color:var(--_comark-muted);font:inherit;cursor:pointer;transition:background-color .15s,color .15s}.ctb-copy-main{width:1.75rem;border-radius:calc(var(--_comark-radius) - 5px) 0 0 calc(var(--_comark-radius) - 5px)}.ctb-copy>.ctb-copy-more{width:1.25rem;border-inline-start:1px solid var(--_comark-border);border-radius:0 calc(var(--_comark-radius) - 5px) calc(var(--_comark-radius) - 5px) 0}.ctb-copy-more svg{width:.75rem;height:.75rem}.ctb-copy-more[aria-expanded=true]{background:var(--_comark-accent);color:var(--_comark-foreground)}@media (hover:hover){.ctb-copy>button:hover,.ctb-menu button:hover{background:var(--_comark-accent);color:var(--_comark-foreground)}}.ctb-copy-main svg+svg,.ctb-copy-main[data-state=done] svg:first-child{display:none}.ctb-copy-main[data-state=done] svg+svg{display:block}.ctb-menu{position:absolute;top:calc(100% + .25rem);right:-1px;z-index:10;display:grid;min-width:15rem;padding:.25rem;border:1px solid var(--_comark-border);border-radius:var(--_comark-radius);background:var(--_comark-background);box-shadow:0 4px 16px rgb(0 0 0/.12),0 1px 3px rgb(0 0 0/.08)}.ctb-menu[hidden]{display:none}.ctb-menu button{display:grid;grid-template-columns:.875rem 1fr;gap:.5rem;align-items:start;padding:.375rem .5rem;border:0;border-radius:calc(var(--_comark-radius) - 4px);background:none;color:var(--_comark-foreground);font:inherit;text-align:start;cursor:pointer}.ctb-menu button:focus-visible{outline:none;background:var(--_comark-accent)}.ctb-menu svg{margin-top:.125rem}.ctb-menu b{display:block;font-weight:500}.ctb-menu small{display:block;color:var(--_comark-muted);font-size:.75rem;font-weight:400;line-height:1rem}.ctb-status{margin-inline-end:.25rem;color:var(--_comark-muted)}.ctb-sort{all:unset}.ctb-sort{margin:0!important;padding:0!important;border:0!important;background:none!important;box-shadow:none!important;font:inherit!important;color:inherit!important;text-align:inherit!important}html[data-table-ready] .ctb-sort{display:inline-flex;align-items:center;gap:.25rem;cursor:pointer}html[data-table-ready] .ctb-sort::after{content:'↕';color:var(--_comark-muted);font-weight:400}html[data-table-ready] [aria-sort='ascending']>.ctb-sort::after{content:'↑';color:inherit}html[data-table-ready] [aria-sort='descending']>.ctb-sort::after{content:'↓';color:inherit}.ctb-copy>button:focus-visible{outline:none;box-shadow:0 0 0 3px color-mix(in srgb,var(--_comark-ring) 50%,transparent)}.ctb-sort:focus-visible{outline:none;border-radius:calc(var(--_comark-radius) - 4px);box-shadow:0 0 0 3px color-mix(in srgb,var(--_comark-ring) 50%,transparent)!important}@media print{.ctb-bar{display:none}.ctb-sort::after{content:none!important}}}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<!-- Tables for `@comark/svelte`. -->
|
|
2
|
+
<script>
|
|
3
|
+
import { onMount } from "svelte";
|
|
4
|
+
import { bar, css, start } from "comark-table/client";
|
|
5
|
+
|
|
6
|
+
let { copy = false, sort = false, children } = $props();
|
|
7
|
+
onMount(() => start(css));
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<div class="ctb">
|
|
11
|
+
{#if copy}
|
|
12
|
+
<!-- Static markup, the same string every renderer uses. -->
|
|
13
|
+
<div class="ctb-bar">{@html bar()}</div>
|
|
14
|
+
{/if}
|
|
15
|
+
<div class="ctb-scroll">{@render children?.()}</div>
|
|
16
|
+
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Component, Snippet } from "svelte";
|
|
2
|
+
import type { TableAttributes } from "comark-table";
|
|
3
|
+
|
|
4
|
+
export { default, TABLE_TAG } from "comark-table";
|
|
5
|
+
export type { TableAttributes, TableMeta, TableOptions, TableReader } from "comark-table";
|
|
6
|
+
|
|
7
|
+
export declare const DataTable: Component<TableAttributes & { children?: Snippet }>;
|
|
8
|
+
export declare const components: { DataTable: typeof DataTable };
|
package/dist/vue.d.mts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import _default, { TABLE_TAG, TableAttributes, TableMeta, TableOptions, TableReader } from "./index.mjs";
|
|
2
|
+
//#region src/vue.d.ts
|
|
3
|
+
/** Tables for `@comark/vue`. */
|
|
4
|
+
export declare const DataTable: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
5
|
+
sort: BooleanConstructor;
|
|
6
|
+
copy: BooleanConstructor;
|
|
7
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
10
|
+
sort: BooleanConstructor;
|
|
11
|
+
copy: BooleanConstructor;
|
|
12
|
+
}>> & Readonly<{}>, {
|
|
13
|
+
sort: boolean;
|
|
14
|
+
copy: boolean;
|
|
15
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
16
|
+
/** Pass these as `components` to `@comark/vue`. */
|
|
17
|
+
export declare const components: {
|
|
18
|
+
DataTable: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
19
|
+
sort: BooleanConstructor;
|
|
20
|
+
copy: BooleanConstructor;
|
|
21
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
24
|
+
sort: BooleanConstructor;
|
|
25
|
+
copy: BooleanConstructor;
|
|
26
|
+
}>> & Readonly<{}>, {
|
|
27
|
+
sort: boolean;
|
|
28
|
+
copy: boolean;
|
|
29
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
30
|
+
};
|
|
31
|
+
//#endregion
|
|
32
|
+
export { TABLE_TAG, type TableAttributes, type TableMeta, type TableOptions, type TableReader, _default as default };
|
package/dist/vue.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import src_default, { TABLE_TAG } from "./index.mjs";
|
|
2
|
+
import { n as start, r as css, t as bar } from "./client-C5bRu9NG.mjs";
|
|
3
|
+
import { defineComponent, h, onBeforeUnmount, onMounted } from "vue";
|
|
4
|
+
//#region src/vue.ts
|
|
5
|
+
/** Tables for `@comark/vue`. */
|
|
6
|
+
const DataTable = defineComponent({
|
|
7
|
+
name: "DataTable",
|
|
8
|
+
inheritAttrs: false,
|
|
9
|
+
props: {
|
|
10
|
+
sort: Boolean,
|
|
11
|
+
copy: Boolean
|
|
12
|
+
},
|
|
13
|
+
setup(props, { slots }) {
|
|
14
|
+
let stop = () => {};
|
|
15
|
+
onMounted(() => stop = start(css));
|
|
16
|
+
onBeforeUnmount(() => stop());
|
|
17
|
+
return () => h("div", { class: "ctb" }, [props.copy ? h("div", {
|
|
18
|
+
class: "ctb-bar",
|
|
19
|
+
innerHTML: bar()
|
|
20
|
+
}) : null, h("div", { class: "ctb-scroll" }, slots.default?.())]);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
/** Pass these as `components` to `@comark/vue`. */
|
|
24
|
+
const components = { DataTable };
|
|
25
|
+
//#endregion
|
|
26
|
+
export { DataTable, TABLE_TAG, components, src_default as default };
|
package/package.json
CHANGED
|
@@ -1,6 +1,91 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "comark-table",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.7.1",
|
|
4
|
+
"description": "Tables from CSV, TSV, and JSON files or fences, read while parsing, with copy buttons and sortable headers. A Comark plugin.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"comark",
|
|
7
|
+
"csv",
|
|
8
|
+
"json",
|
|
9
|
+
"markdown",
|
|
10
|
+
"mdc",
|
|
11
|
+
"plugin",
|
|
12
|
+
"table",
|
|
13
|
+
"tsv"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://comark-ui.onmax.me/table",
|
|
16
|
+
"bugs": "https://github.com/onmax/comark-ui/issues",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "onmax",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/onmax/comark-ui.git",
|
|
22
|
+
"directory": "packages/table"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"type": "module",
|
|
28
|
+
"sideEffects": [
|
|
29
|
+
"*.css"
|
|
30
|
+
],
|
|
31
|
+
"exports": {
|
|
32
|
+
".": "./dist/index.mjs",
|
|
33
|
+
"./client": "./dist/client.mjs",
|
|
34
|
+
"./html": "./dist/html.mjs",
|
|
35
|
+
"./react": "./dist/react.mjs",
|
|
36
|
+
"./style.css": "./dist/style.css",
|
|
37
|
+
"./svelte": {
|
|
38
|
+
"types": "./dist/svelte/index.d.ts",
|
|
39
|
+
"svelte": "./dist/svelte/index.js",
|
|
40
|
+
"default": "./dist/svelte/index.js"
|
|
41
|
+
},
|
|
42
|
+
"./vue": "./dist/vue.mjs",
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"provenance": true
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@comark/html": "^0.8.1",
|
|
51
|
+
"@comark/react": "^0.8.1",
|
|
52
|
+
"@comark/vue": "^0.8.1",
|
|
53
|
+
"@types/react": "^19.3.0",
|
|
54
|
+
"@types/react-dom": "^19.3.0",
|
|
55
|
+
"comark": "^0.8.1",
|
|
56
|
+
"react": "^19.3.1",
|
|
57
|
+
"react-dom": "^19.3.1",
|
|
58
|
+
"svelte": "^5.57.2",
|
|
59
|
+
"vue": "^3.5.44",
|
|
60
|
+
"comark-zoom": "0.7.1"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@comark/html": "^0.8.0",
|
|
64
|
+
"comark": "^0.8.0",
|
|
65
|
+
"react": ">=18",
|
|
66
|
+
"svelte": ">=5",
|
|
67
|
+
"vue": ">=3.5"
|
|
68
|
+
},
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@comark/html": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"react": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"svelte": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"vue": {
|
|
80
|
+
"optional": true
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"engines": {
|
|
84
|
+
"node": ">=22.12.0"
|
|
85
|
+
},
|
|
86
|
+
"scripts": {
|
|
87
|
+
"build": "tsdown",
|
|
88
|
+
"dev": "tsdown --watch",
|
|
89
|
+
"doctor": "vite-doctor . --no-cache --max-warnings 0 --format text"
|
|
90
|
+
}
|
|
6
91
|
}
|