@k2b/editor 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/editor",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Configurable Markdown editor for SolidJS: editor, toolbar, bubble menu, and slash menu on one shared state.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"@codemirror/state": "6.7.1",
|
|
70
70
|
"@codemirror/view": "6.43.7",
|
|
71
71
|
"@k2b/stdlib": "0.30.0",
|
|
72
|
-
"@k2b/ui": "0.38.
|
|
72
|
+
"@k2b/ui": "0.38.1",
|
|
73
73
|
"@lezer/common": "1.5.3",
|
|
74
74
|
"@lezer/highlight": "1.2.4",
|
|
75
75
|
"@lezer/markdown": "1.7.2",
|
|
@@ -2,6 +2,7 @@ import { type EditorState, type Extension, Prec, type Range, RangeSet, StateFiel
|
|
|
2
2
|
import { Decoration, type DecorationSet, EditorView, keymap, WidgetType } from "@codemirror/view";
|
|
3
3
|
import { holdsDecorations, refreshMarkdownDecorationsEffect, selectionIntersectsRange } from "./_lib/cursor-zone-field";
|
|
4
4
|
import { type DataBlock, extractDataBlocks, namedBlockBody, renderDataBlockHtml } from "./lib/named-blocks";
|
|
5
|
+
import { blockAnchorsDrawnElsewhere } from "./named-blocks";
|
|
5
6
|
|
|
6
7
|
type DataBlockRange = {
|
|
7
8
|
from: number;
|
|
@@ -52,8 +53,10 @@ class DataBlockWidget extends WidgetType {
|
|
|
52
53
|
|
|
53
54
|
override get estimatedHeight() {
|
|
54
55
|
const rows = Math.max(1, this.body.split("\n").filter((line) => line.trim().length > 0).length);
|
|
55
|
-
// 12px / 16px cells + 16px padding + divider; the
|
|
56
|
-
|
|
56
|
+
// 12px / 16px cells + 16px padding + divider per row; the anchor label (18px line), its margins and the outer
|
|
57
|
+
// padding take just under 44px. Chromium and WebKit round the rows differently (one and two rows measured
|
|
58
|
+
// 75.98 / 109.19px and 109.89px), so the base keeps the estimate at most 1px above both.
|
|
59
|
+
return 43.95 + rows * 33;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
override ignoreEvent() {
|
|
@@ -76,6 +79,8 @@ export const dataBlocksExtension = (): Extension => {
|
|
|
76
79
|
},
|
|
77
80
|
provide(field) {
|
|
78
81
|
return [
|
|
82
|
+
// The data block widget draws its anchor label together with the block.
|
|
83
|
+
blockAnchorsDrawnElsewhere.of("data"),
|
|
79
84
|
EditorView.decorations.from(field, (value) => value.decorations),
|
|
80
85
|
EditorView.atomicRanges.of((view) => view.state.field(field).blockWidgetDecorations),
|
|
81
86
|
];
|
|
@@ -354,7 +354,7 @@ const humanizeKey = (key: string): string =>
|
|
|
354
354
|
export const renderDataBlockHtml = (name: string | null, src: string): string => {
|
|
355
355
|
const entries = parseNamedDataBlock(src);
|
|
356
356
|
const handle = name
|
|
357
|
-
? `<div class="md-block-handle" data-block-name="${escapeHtml(name)}"
|
|
357
|
+
? `<div class="md-block-handle" data-block-name="${escapeHtml(name)}">${escapeHtml(name)}</div>`
|
|
358
358
|
: `<div class="md-data-handle-row"><div class="md-block-handle">data</div></div>`;
|
|
359
359
|
const rows =
|
|
360
360
|
entries.length > 0
|
|
@@ -392,7 +392,7 @@ export const renderNamedBlockHandlesMarkdown = (md: string | null | undefined):
|
|
|
392
392
|
}
|
|
393
393
|
for (const block of extractNamedBlocks(md)) {
|
|
394
394
|
if (block.type === "data") continue;
|
|
395
|
-
const handle = `<div class="md-block-handle" data-block-name="${escapeHtml(block.name)}"
|
|
395
|
+
const handle = `<div class="md-block-handle" data-block-name="${escapeHtml(block.name)}">${escapeHtml(block.name)}</div>`;
|
|
396
396
|
replacements.push({ from: block.handleStart, to: block.handleEnd, html: handle });
|
|
397
397
|
}
|
|
398
398
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Extension, type Range, RangeSet } from "@codemirror/state";
|
|
1
|
+
import { type Extension, Facet, type Range, RangeSet } from "@codemirror/state";
|
|
2
2
|
import { Decoration, EditorView, WidgetType } from "@codemirror/view";
|
|
3
3
|
import {
|
|
4
4
|
blockWidgetLineNavigationExtension,
|
|
@@ -8,6 +8,13 @@ import {
|
|
|
8
8
|
} from "./_lib/cursor-zone-field";
|
|
9
9
|
import { extractNamedBlocks } from "./lib/named-blocks";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Block types whose anchor line another extension draws together with the block, such as data blocks. The anchor
|
|
13
|
+
* label leaves those lines alone; every other named block (tables, lists, …) gets the label.
|
|
14
|
+
*/
|
|
15
|
+
export const blockAnchorsDrawnElsewhere = Facet.define<string, ReadonlySet<string>>({ combine: (types) => new Set(types) });
|
|
16
|
+
|
|
17
|
+
/** The anchor line `@name` above a block, shown as a quiet label until the caret enters the line. */
|
|
11
18
|
class NamedBlockHandleWidget extends WidgetType {
|
|
12
19
|
constructor(
|
|
13
20
|
private name: string,
|
|
@@ -19,8 +26,9 @@ class NamedBlockHandleWidget extends WidgetType {
|
|
|
19
26
|
override toDOM(view: EditorView) {
|
|
20
27
|
const el = document.createElement("div");
|
|
21
28
|
el.className = "cm-named-block-handle md-block-handle";
|
|
29
|
+
el.dataset.blockName = this.name;
|
|
22
30
|
el.setAttribute("contenteditable", "false");
|
|
23
|
-
el.textContent =
|
|
31
|
+
el.textContent = this.name;
|
|
24
32
|
el.onmousedown = (event) => {
|
|
25
33
|
event.preventDefault();
|
|
26
34
|
event.stopPropagation();
|
|
@@ -52,9 +60,7 @@ export const namedBlocksExtension = (): Extension => {
|
|
|
52
60
|
const nextLine = state.doc.lineAt(Math.min(block.handleEnd + 1, state.doc.length));
|
|
53
61
|
const sourceVisibleRange = { from: prevLine.from, to: nextLine.to };
|
|
54
62
|
ranges.push(sourceVisibleRange);
|
|
55
|
-
|
|
56
|
-
// with the preview so the handle and body collapse as one stable block.
|
|
57
|
-
if (block.type === "table" || block.type === "data") continue;
|
|
63
|
+
if (state.facet(blockAnchorsDrawnElsewhere).has(block.type)) continue;
|
|
58
64
|
if (selectionIntersectsRange(cursor, sourceVisibleRange.from, sourceVisibleRange.to)) continue;
|
|
59
65
|
const handleDecoration = Decoration.replace({
|
|
60
66
|
widget: new NamedBlockHandleWidget(block.name, block.handleStart),
|
|
@@ -205,7 +205,7 @@ export const createPrettyTablePresentation = (data: PrettyTableData, options: Pr
|
|
|
205
205
|
export const renderPrettyTableHtml = (data: PrettyTableData, options: PrettyTableOptions = {}): string => {
|
|
206
206
|
const presentation = createPrettyTablePresentation(data, options);
|
|
207
207
|
const caption = data.caption
|
|
208
|
-
? `<div class="md-block-handle" data-block-name="${escapeHtml(data.caption)}"
|
|
208
|
+
? `<div class="md-block-handle" data-block-name="${escapeHtml(data.caption)}">${escapeHtml(data.caption)}</div>`
|
|
209
209
|
: "";
|
|
210
210
|
const headerHtml = data.headers.map((_, i) => `<th scope="col">${presentation.headerHtml(i)}</th>`).join("");
|
|
211
211
|
// An all-empty GFM header is the approved header-off representation.
|
|
@@ -7,7 +7,6 @@ import type { TableHost } from "@k2b/ui/text-editor";
|
|
|
7
7
|
import { displayCell, findTables, type Table } from "@k2b/ui/text-editor/model";
|
|
8
8
|
import { isFormula } from "../formula";
|
|
9
9
|
import { refreshMarkdownDecorationsEffect } from "./_lib/cursor-zone-field";
|
|
10
|
-
import { extractNamedBlocks } from "./lib/named-blocks";
|
|
11
10
|
import { applyLigatures } from "./ligatures";
|
|
12
11
|
import { createPrettyTablePresentation, formatFormulaValue, type PrettyTableData, type PrettyTableOptions } from "./pretty-table";
|
|
13
12
|
import { formulaPreviewWidget } from "./tables";
|
|
@@ -33,30 +32,6 @@ const presentationData = (table: Table): PrettyTableData => {
|
|
|
33
32
|
return { headers: values(table.lines[0]), rows: table.lines.slice(2).map(values), align: table.align };
|
|
34
33
|
};
|
|
35
34
|
|
|
36
|
-
const captionDecorations = (state: EditorState): DecorationSet => {
|
|
37
|
-
const source = state.doc.toString();
|
|
38
|
-
const starts = new Set(findTables(source).map((table) => table.from));
|
|
39
|
-
return Decoration.set(
|
|
40
|
-
extractNamedBlocks(source)
|
|
41
|
-
.filter((block) => block.type === "table" && starts.has(block.blockStart))
|
|
42
|
-
.map((block) =>
|
|
43
|
-
// Keep the actual handle editable. namedBlocksExtension skips table handles;
|
|
44
|
-
// tableGrid starts on the header, so neither extension renders a second caption.
|
|
45
|
-
Decoration.mark({ class: "md-block-handle cm-notebook-table-caption", attributes: { "data-block-name": block.name } }).range(
|
|
46
|
-
block.handleStart,
|
|
47
|
-
block.handleEnd,
|
|
48
|
-
),
|
|
49
|
-
),
|
|
50
|
-
true,
|
|
51
|
-
);
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const tableCaptions = StateField.define<DecorationSet>({
|
|
55
|
-
create: captionDecorations,
|
|
56
|
-
update: (value, tr) => (tr.docChanged ? captionDecorations(tr.state) : value),
|
|
57
|
-
provide: (field) => EditorView.decorations.from(field),
|
|
58
|
-
});
|
|
59
|
-
|
|
60
35
|
const presentationTheme = EditorView.theme({
|
|
61
36
|
".k2b-ste-table .cm-notebook-formula-editor": {
|
|
62
37
|
position: "absolute",
|
|
@@ -85,7 +60,6 @@ const presentationTheme = EditorView.theme({
|
|
|
85
60
|
|
|
86
61
|
".k2b-ste-table .k2b-ste-cell > .md-table-cell": { padding: "0", minHeight: "0" },
|
|
87
62
|
".k2b-ste-table tr:has([data-notebook-total]) > td": { borderTop: "1px solid var(--ui-border-strong)", fontWeight: "600" },
|
|
88
|
-
".cm-notebook-table-caption": { margin: "0" },
|
|
89
63
|
".cm-notebook-formula-copy": {
|
|
90
64
|
display: "inline-flex",
|
|
91
65
|
alignItems: "center",
|
|
@@ -216,7 +190,7 @@ export const createNotebookTablePresentation = (
|
|
|
216
190
|
} => {
|
|
217
191
|
const snapshots = new WeakMap<Table, { locale: string; presentation: ReturnType<typeof createPrettyTablePresentation> }>();
|
|
218
192
|
return {
|
|
219
|
-
extension: [
|
|
193
|
+
extension: [presentationTheme, activeFormulaPreview(options)],
|
|
220
194
|
renderCell(element, _raw, table, row, column) {
|
|
221
195
|
const locale = options.locale?.() ?? (element.ownerDocument.documentElement.lang || "en");
|
|
222
196
|
let snapshot = snapshots.get(table);
|
package/src/styles.css
CHANGED
|
@@ -142,25 +142,19 @@
|
|
|
142
142
|
display: none !important;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/* The anchor of a named block (`@name` in the source) reads as a quiet label above the block, in the label type of
|
|
146
|
+
form fields. The caret in its line shows the source again. */
|
|
145
147
|
.md-block-handle {
|
|
146
148
|
display: inline-flex;
|
|
147
149
|
align-items: center;
|
|
148
150
|
width: fit-content;
|
|
149
151
|
max-width: 100%;
|
|
150
152
|
margin: 0.35rem 0 0.15rem;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
background: rgb(239 246 255);
|
|
154
|
-
color: rgb(37 99 235);
|
|
155
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
|
156
|
-
font-size: 0.6875rem;
|
|
153
|
+
color: var(--k2b-text-muted);
|
|
154
|
+
font-size: 0.8125rem;
|
|
157
155
|
font-weight: 600;
|
|
158
|
-
line-height: 1.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
.dark .md-block-handle {
|
|
162
|
-
background: rgb(30 58 138 / 0.25);
|
|
163
|
-
color: rgb(147 197 253);
|
|
156
|
+
line-height: 1.125rem;
|
|
157
|
+
overflow-wrap: anywhere;
|
|
164
158
|
}
|
|
165
159
|
|
|
166
160
|
.md-data-handle-row {
|