testomatio-editor-blocks 0.4.79 → 0.4.82

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.
@@ -0,0 +1,148 @@
1
+ import { BlockNoteExtension } from "@blocknote/core";
2
+ import type { Node as PMNode } from "prosemirror-model";
3
+ import { Plugin, PluginKey } from "prosemirror-state";
4
+ import { Decoration, DecorationSet } from "prosemirror-view";
5
+
6
+ /**
7
+ * An "example table" is a data table that belongs to a testomat.io `<!-- example -->`
8
+ * marker: it sits between that marker and the start of the next test — i.e. the next
9
+ * `testMeta` block (`<!-- test ... -->` / `<!-- suite ... -->`) — or the end of the
10
+ * document. Such tables are painted with a gray cell background so they read as the
11
+ * marker's data table.
12
+ *
13
+ * Given the block content-type names in document order, returns a boolean[] where
14
+ * entry `i` is `true` iff a `"table"` at index `i` falls inside an example region.
15
+ * `exampleMarker` opens a region; `testMeta` closes it; everything else (headings,
16
+ * paragraphs, …) leaves the region unchanged.
17
+ *
18
+ * Pure and DOM-free so it can be unit-tested directly.
19
+ */
20
+ export function markExampleTables(types: readonly string[]): boolean[] {
21
+ const flags: boolean[] = [];
22
+ let active = false;
23
+ for (const name of types) {
24
+ if (name === "exampleMarker") {
25
+ active = true;
26
+ flags.push(false);
27
+ } else if (name === "testMeta") {
28
+ active = false;
29
+ flags.push(false);
30
+ } else {
31
+ flags.push(name === "table" && active);
32
+ }
33
+ }
34
+ return flags;
35
+ }
36
+
37
+ /**
38
+ * Example tables only occur in a *suite* document — one that opens with a
39
+ * `<!-- suite ... -->` comment (a `testMeta` block whose `metaKind` is `"suite"`).
40
+ * Gating on the first block lets the plugin skip the whole scan for any other
41
+ * document (a lone test, plain notes, …) instead of walking it on every change.
42
+ *
43
+ * Pure so it can be unit-tested directly.
44
+ */
45
+ export function isSuiteDocument(
46
+ firstBlockType: string | undefined,
47
+ firstBlockMetaKind: string | undefined,
48
+ ): boolean {
49
+ return firstBlockType === "testMeta" && firstBlockMetaKind === "suite";
50
+ }
51
+
52
+ /**
53
+ * Build node decorations that add `.bn-example-table` to every table in an example
54
+ * region. Tables are only *painted* — the document is untouched, so markdown
55
+ * serialization round-trips unchanged.
56
+ *
57
+ * The `<!-- example -->` marker, the metadata comment, and the table each become a
58
+ * top-level block whose content node is named by its block type (`exampleMarker`,
59
+ * `testMeta`, `table`), so a single document-order walk yields them directly.
60
+ *
61
+ * Nothing is highlighted unless the document is a suite (see `isSuiteDocument`).
62
+ */
63
+ function buildExampleTableDecorations(doc: PMNode): DecorationSet {
64
+ const entries: { pos: number; size: number; name: string }[] = [];
65
+ let firstBlockType: string | undefined;
66
+ let firstBlockMetaKind: string | undefined;
67
+
68
+ doc.descendants((node, pos, parent) => {
69
+ // The first content node encountered under a blockContainer is the document's
70
+ // first block — capture its type/metaKind for the suite gate below.
71
+ if (firstBlockType === undefined && parent?.type.name === "blockContainer") {
72
+ firstBlockType = node.type.name;
73
+ const metaKind = node.attrs?.metaKind;
74
+ firstBlockMetaKind = typeof metaKind === "string" ? metaKind : undefined;
75
+ }
76
+
77
+ const name = node.type.name;
78
+ if (name === "exampleMarker" || name === "testMeta" || name === "table") {
79
+ entries.push({ pos, size: node.nodeSize, name });
80
+ // No need to descend into table cells or the marker's (empty) internals.
81
+ return false;
82
+ }
83
+ // Descend through the blockGroup / blockContainer wrappers to reach content.
84
+ return undefined;
85
+ });
86
+
87
+ if (!isSuiteDocument(firstBlockType, firstBlockMetaKind)) {
88
+ return DecorationSet.empty;
89
+ }
90
+
91
+ const flags = markExampleTables(entries.map((entry) => entry.name));
92
+ const decorations = entries
93
+ .filter((_, index) => flags[index])
94
+ .map((entry) =>
95
+ Decoration.node(entry.pos, entry.pos + entry.size, {
96
+ class: "bn-example-table",
97
+ }),
98
+ );
99
+
100
+ return DecorationSet.create(doc, decorations);
101
+ }
102
+
103
+ const exampleTablePluginKey = new PluginKey<DecorationSet>("testomatioExampleTable");
104
+
105
+ function exampleTablePlugin(): Plugin<DecorationSet> {
106
+ return new Plugin<DecorationSet>({
107
+ key: exampleTablePluginKey,
108
+ state: {
109
+ init: (_config, state) => buildExampleTableDecorations(state.doc),
110
+ apply: (tr, value) =>
111
+ tr.docChanged ? buildExampleTableDecorations(tr.doc) : value,
112
+ },
113
+ props: {
114
+ decorations(state) {
115
+ return exampleTablePluginKey.getState(state);
116
+ },
117
+ },
118
+ });
119
+ }
120
+
121
+ /**
122
+ * BlockNote extension that grays the cells of the table following an
123
+ * `<!-- example -->` marker (bounded by the next test/suite comment or the end of
124
+ * the document).
125
+ *
126
+ * Editor extensions are supplied at editor-creation time and cannot be carried by
127
+ * the schema, so consumers must add this to their `useCreateBlockNote` call:
128
+ *
129
+ * ```ts
130
+ * useCreateBlockNote({
131
+ * schema: customSchema,
132
+ * extensions: [exampleTableHighlightExtension()],
133
+ * });
134
+ * ```
135
+ */
136
+ export class ExampleTableHighlightExtension extends BlockNoteExtension {
137
+ static key() {
138
+ return "exampleTableHighlight";
139
+ }
140
+
141
+ constructor() {
142
+ super();
143
+ this.addProsemirrorPlugin(exampleTablePlugin());
144
+ }
145
+ }
146
+
147
+ /** Factory for the `extensions` option of `useCreateBlockNote`. */
148
+ export const exampleTableHighlightExtension = () => new ExampleTableHighlightExtension();
@@ -771,6 +771,26 @@ html.dark .testomatio-editor [data-content-type="heading"] .bn-tag-badge {
771
771
  border-color: rgba(255, 255, 255, 0.12);
772
772
  }
773
773
 
774
+ /* ============================================
775
+ AUTO-LINKED URLS
776
+ Bare `http(s)://` URLs are painted as links by a ProseMirror inline
777
+ decoration (see src/editor/autoLink.ts). The text stays editable and the
778
+ markdown is untouched; only the styling and a Cmd/Ctrl+click handler are
779
+ added. Opening on click is wired in the plugin, not via `pointer-events`.
780
+ ============================================ */
781
+ .testomatio-editor .bn-auto-link {
782
+ color: var(--color-primary-700);
783
+ text-decoration: underline;
784
+ text-underline-offset: 2px;
785
+ cursor: pointer;
786
+ }
787
+ .testomatio-editor .bn-auto-link:hover {
788
+ color: var(--color-primary-600);
789
+ }
790
+ html.dark .testomatio-editor .bn-auto-link {
791
+ color: rgba(129, 140, 248, 1);
792
+ }
793
+
774
794
  /* ============================================
775
795
  TEST / SUITE METADATA BLOCK
776
796
  Dimmed card for `<!-- test ... -->` / `<!-- suite ... -->` comments.
@@ -791,6 +811,37 @@ html.dark .testomatio-editor [data-content-type="heading"] .bn-tag-badge {
791
811
  opacity: 0.5;
792
812
  }
793
813
 
814
+ /* Compact marker for `<!-- example -->` — a cropped variant of the testMeta bar
815
+ (fit-content width, not full editor width) labelling the table below it. */
816
+ .bn-example-marker {
817
+ display: inline-flex;
818
+ align-items: center;
819
+ width: fit-content;
820
+ box-sizing: border-box;
821
+ padding: 4px 12px;
822
+ background: var(--bg-muted);
823
+ border-top: 3px solid var(--color-slate-400);
824
+ margin-top: 1rem;
825
+ opacity: 0.6;
826
+ }
827
+
828
+ .bn-example-marker__label {
829
+ font-size: 11px;
830
+ font-weight: 600;
831
+ letter-spacing: 0.04em;
832
+ text-transform: uppercase;
833
+ color: var(--text-muted);
834
+ }
835
+
836
+ /* Gray cells for the data table that follows an `<!-- example -->` marker. The
837
+ `.bn-example-table` class is applied by a ProseMirror decoration (see
838
+ exampleTableHighlight.ts), bounded to tables between the marker and the next
839
+ test/suite comment (or end of document). */
840
+ .bn-editor [data-content-type="table"].bn-example-table td,
841
+ .bn-editor [data-content-type="table"].bn-example-table th {
842
+ background: var(--bg-muted);
843
+ }
844
+
794
845
  /* Header line: `TEST @T1233456 ............ [+]` — label, id, and add button
795
846
  always share one row. */
796
847
  .bn-testmeta__header {
@@ -1932,6 +1983,20 @@ html.dark .bn-step-editor--preview a.step-preview-link {
1932
1983
  font-size: var(--badge-fz-sm);
1933
1984
  }
1934
1985
 
1986
+ .bn-mantine .bn-suggestion-menu {
1987
+ max-width: 24rem;
1988
+ }
1989
+
1990
+ .bn-mt-suggestion-menu-item-body {
1991
+ min-width: 0;
1992
+ }
1993
+
1994
+ .bn-mt-suggestion-menu-item-subtitle {
1995
+ white-space: nowrap;
1996
+ overflow: hidden;
1997
+ text-overflow: ellipsis;
1998
+ }
1999
+
1935
2000
  .bn-suggestion-icon {
1936
2001
  display: inline-flex;
1937
2002
  align-items: center;
package/src/index.ts CHANGED
@@ -23,6 +23,19 @@ export {
23
23
  type TagMatch,
24
24
  } from "./editor/tagBadge";
25
25
 
26
+ export {
27
+ autoLinkExtension,
28
+ AutoLinkExtension,
29
+ detectLinks,
30
+ type LinkMatch,
31
+ } from "./editor/autoLink";
32
+
33
+ export {
34
+ exampleTableHighlightExtension,
35
+ ExampleTableHighlightExtension,
36
+ markExampleTables,
37
+ } from "./editor/exampleTableHighlight";
38
+
26
39
  export {
27
40
  blocksToMarkdown,
28
41
  markdownToBlocks,