sonorance 0.1.0-beta.4.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 +174 -0
- package/README.md +103 -0
- package/build/icon.png +0 -0
- package/package.json +86 -0
- package/skill/SKILL.md +41 -0
- package/skill/scripts/sonorance.mjs +31 -0
- package/src/azure-monitor.mjs +221 -0
- package/src/cli.mjs +315 -0
- package/src/comment-client.mjs +75 -0
- package/src/engine-default.mjs +136 -0
- package/src/feedback.mjs +151 -0
- package/src/gitignore.mjs +27 -0
- package/src/grammar.mjs +55 -0
- package/src/identity.mjs +126 -0
- package/src/otlp.mjs +166 -0
- package/src/plugins/deliberate/contribute.mjs +28 -0
- package/src/plugins/deliberate/domain.mjs +40 -0
- package/src/plugins/deliberate/frontmatter.mjs +85 -0
- package/src/plugins/deliberate/gitignore.mjs +21 -0
- package/src/plugins/deliberate/kinds.mjs +44 -0
- package/src/plugins/deliberate/markdown.mjs +42 -0
- package/src/plugins/deliberate/paths.mjs +245 -0
- package/src/plugins/deliberate/stages.mjs +27 -0
- package/src/plugins/deliberate/vault.mjs +1043 -0
- package/src/plugins.mjs +91 -0
- package/src/release-config.mjs +2 -0
- package/src/scrubber.mjs +64 -0
- package/src/server/index.mjs +993 -0
- package/src/sources.mjs +80 -0
- package/src/telemetry-schema.mjs +187 -0
- package/src/telemetry.mjs +390 -0
- package/src/ui/active-line.mjs +42 -0
- package/src/ui/app.js +3553 -0
- package/src/ui/at-mention.mjs +67 -0
- package/src/ui/comments-plugin.mjs +107 -0
- package/src/ui/diff-plugin.mjs +73 -0
- package/src/ui/editor.mjs +210 -0
- package/src/ui/index.html +1723 -0
- package/src/ui/md.mjs +233 -0
- package/src/ui/paste-md.mjs +54 -0
- package/src/ui/shell.html +1374 -0
- package/src/ui/slash.mjs +122 -0
- package/src/vault-registry.mjs +150 -0
package/src/ui/md.mjs
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// Markdown <-> ProseMirror(Tiptap) bridge, built on remark/unified.
|
|
2
|
+
//
|
|
3
|
+
// The editor's in-memory truth is a ProseMirror document; the file on disk is
|
|
4
|
+
// Markdown. This module is the only place that converts between them:
|
|
5
|
+
//
|
|
6
|
+
// mdToDoc(md) -> { frontmatter, doc } (Markdown -> Tiptap JSON)
|
|
7
|
+
// docToMd(doc, frontmatter) -> md string (Tiptap JSON -> Markdown)
|
|
8
|
+
//
|
|
9
|
+
// Byte-fidelity is NOT a goal: serialization is opinionated (remark-stringify
|
|
10
|
+
// via mdast-util-to-markdown). Leading YAML frontmatter is split out verbatim
|
|
11
|
+
// so machine-state stays hidden from the reading view and is re-attached on
|
|
12
|
+
// save.
|
|
13
|
+
import { unified } from 'unified';
|
|
14
|
+
import remarkParse from 'remark-parse';
|
|
15
|
+
import remarkGfm from 'remark-gfm';
|
|
16
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
17
|
+
import { toMarkdown } from 'mdast-util-to-markdown';
|
|
18
|
+
import { gfmToMarkdown } from 'mdast-util-gfm';
|
|
19
|
+
|
|
20
|
+
const parser = unified().use(remarkParse).use(remarkGfm).use(remarkFrontmatter, ['yaml']);
|
|
21
|
+
|
|
22
|
+
// ---- Markdown -> Tiptap JSON ----------------------------------------------
|
|
23
|
+
|
|
24
|
+
// Split a leading YAML frontmatter block (--- … ---) off the raw source so it
|
|
25
|
+
// can be preserved verbatim and hidden. Returns { frontmatter, body }.
|
|
26
|
+
export function splitFrontmatter(md) {
|
|
27
|
+
const m = /^(---\n[\s\S]*?\n---)(\n|$)/.exec(md);
|
|
28
|
+
if (!m) return { frontmatter: '', body: md };
|
|
29
|
+
return { frontmatter: m[1], body: md.slice(m[0].length) };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const MARK = { strong: 'bold', emphasis: 'italic', inlineCode: 'code', delete: 'strike' };
|
|
33
|
+
|
|
34
|
+
function inlineChildren(node, marks = []) {
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const c of node.children || []) out.push(...inlineNode(c, marks));
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function inlineNode(n, marks) {
|
|
41
|
+
switch (n.type) {
|
|
42
|
+
case 'text':
|
|
43
|
+
return n.value ? [{ type: 'text', text: n.value, ...(marks.length ? { marks } : {}) }] : [];
|
|
44
|
+
case 'strong': case 'emphasis': case 'delete':
|
|
45
|
+
return inlineChildren(n, [...marks, { type: MARK[n.type] }]);
|
|
46
|
+
case 'inlineCode':
|
|
47
|
+
return [{ type: 'text', text: n.value, marks: [...marks, { type: 'code' }] }];
|
|
48
|
+
case 'link':
|
|
49
|
+
return inlineChildren(n, [...marks, { type: 'link', attrs: { href: n.url, title: n.title || null } }]);
|
|
50
|
+
case 'image':
|
|
51
|
+
return [{ type: 'image', attrs: { src: n.url, alt: n.alt || null, title: n.title || null } }];
|
|
52
|
+
case 'break':
|
|
53
|
+
return [{ type: 'hardBreak' }];
|
|
54
|
+
case 'html':
|
|
55
|
+
return n.value ? [{ type: 'text', text: n.value }] : [];
|
|
56
|
+
default:
|
|
57
|
+
return n.children ? inlineChildren(n, marks) : (n.value ? [{ type: 'text', text: n.value, ...(marks.length ? { marks } : {}) }] : []);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function listToBlock(node) {
|
|
62
|
+
const isTask = (node.children || []).some(li => li.checked === true || li.checked === false);
|
|
63
|
+
if (isTask) {
|
|
64
|
+
return {
|
|
65
|
+
type: 'taskList',
|
|
66
|
+
content: (node.children || []).map(li => ({
|
|
67
|
+
type: 'taskItem',
|
|
68
|
+
attrs: { checked: li.checked === true },
|
|
69
|
+
content: blockChildren(li),
|
|
70
|
+
})),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
type: node.ordered ? 'orderedList' : 'bulletList',
|
|
75
|
+
...(node.ordered && node.start != null && node.start !== 1 ? { attrs: { start: node.start } } : {}),
|
|
76
|
+
content: (node.children || []).map(li => ({ type: 'listItem', content: blockChildren(li) })),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function tableToBlock(node) {
|
|
81
|
+
const align = node.align || [];
|
|
82
|
+
const rows = (node.children || []).map((row, ri) => ({
|
|
83
|
+
type: 'tableRow',
|
|
84
|
+
content: (row.children || []).map(cell => ({
|
|
85
|
+
type: ri === 0 ? 'tableHeader' : 'tableCell',
|
|
86
|
+
attrs: { colspan: 1, rowspan: 1, colwidth: null },
|
|
87
|
+
content: [{ type: 'paragraph', content: inlineChildren(cell) }],
|
|
88
|
+
})),
|
|
89
|
+
}));
|
|
90
|
+
return { type: 'table', content: rows };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function blockNode(n) {
|
|
94
|
+
switch (n.type) {
|
|
95
|
+
case 'heading':
|
|
96
|
+
return { type: 'heading', attrs: { level: n.depth }, content: inlineChildren(n) };
|
|
97
|
+
case 'paragraph': {
|
|
98
|
+
const content = inlineChildren(n);
|
|
99
|
+
return { type: 'paragraph', ...(content.length ? { content } : {}) };
|
|
100
|
+
}
|
|
101
|
+
case 'blockquote':
|
|
102
|
+
return { type: 'blockquote', content: blockChildren(n) };
|
|
103
|
+
case 'list':
|
|
104
|
+
return listToBlock(n);
|
|
105
|
+
case 'code':
|
|
106
|
+
return { type: 'codeBlock', attrs: { language: n.lang || null }, content: n.value ? [{ type: 'text', text: n.value }] : [] };
|
|
107
|
+
case 'thematicBreak':
|
|
108
|
+
return { type: 'horizontalRule' };
|
|
109
|
+
case 'table':
|
|
110
|
+
return tableToBlock(n);
|
|
111
|
+
case 'html':
|
|
112
|
+
// Keep raw/unmodeled HTML as a fenced code block so nothing is silently dropped.
|
|
113
|
+
return { type: 'codeBlock', attrs: { language: 'html' }, content: n.value ? [{ type: 'text', text: n.value }] : [] };
|
|
114
|
+
default:
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function blockChildren(node) {
|
|
120
|
+
const out = [];
|
|
121
|
+
for (const c of node.children || []) {
|
|
122
|
+
const b = blockNode(c);
|
|
123
|
+
if (b) out.push(b);
|
|
124
|
+
}
|
|
125
|
+
return out.length ? out : [{ type: 'paragraph' }];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function mdToDoc(md) {
|
|
129
|
+
const { frontmatter, body } = splitFrontmatter(md);
|
|
130
|
+
const tree = parser.parse(body);
|
|
131
|
+
const content = [];
|
|
132
|
+
for (const c of tree.children || []) {
|
|
133
|
+
if (c.type === 'yaml') continue; // belt-and-suspenders; frontmatter already split
|
|
134
|
+
const b = blockNode(c);
|
|
135
|
+
if (b) content.push(b);
|
|
136
|
+
}
|
|
137
|
+
return { frontmatter, doc: { type: 'doc', content: content.length ? content : [{ type: 'paragraph' }] } };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- Tiptap JSON -> Markdown ----------------------------------------------
|
|
141
|
+
|
|
142
|
+
const MARK_MDAST = {
|
|
143
|
+
bold: (kids) => ({ type: 'strong', children: kids }),
|
|
144
|
+
italic: (kids) => ({ type: 'emphasis', children: kids }),
|
|
145
|
+
strike: (kids) => ({ type: 'delete', children: kids }),
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
function textToMdast(node) {
|
|
149
|
+
const text = { type: 'text', value: node.text || '' };
|
|
150
|
+
const marks = node.marks || [];
|
|
151
|
+
if (marks.some(m => m.type === 'code')) {
|
|
152
|
+
let n = { type: 'inlineCode', value: node.text || '' };
|
|
153
|
+
return wrapLinkStrong(n, marks);
|
|
154
|
+
}
|
|
155
|
+
return wrapLinkStrong(text, marks);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function wrapLinkStrong(inner, marks) {
|
|
159
|
+
let n = inner;
|
|
160
|
+
for (const m of marks) {
|
|
161
|
+
if (m.type === 'code') continue;
|
|
162
|
+
if (m.type === 'bold') n = { type: 'strong', children: [n] };
|
|
163
|
+
else if (m.type === 'italic') n = { type: 'emphasis', children: [n] };
|
|
164
|
+
else if (m.type === 'strike') n = { type: 'delete', children: [n] };
|
|
165
|
+
else if (m.type === 'link') n = { type: 'link', url: m.attrs?.href || '', title: m.attrs?.title || null, children: [n] };
|
|
166
|
+
}
|
|
167
|
+
return n;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function inlineToMdast(nodes) {
|
|
171
|
+
const out = [];
|
|
172
|
+
for (const n of nodes || []) {
|
|
173
|
+
if (n.type === 'text') out.push(textToMdast(n));
|
|
174
|
+
else if (n.type === 'hardBreak') out.push({ type: 'break' });
|
|
175
|
+
else if (n.type === 'image') out.push({ type: 'image', url: n.attrs?.src || '', alt: n.attrs?.alt || null, title: n.attrs?.title || null });
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function blockToMdast(n) {
|
|
181
|
+
switch (n.type) {
|
|
182
|
+
case 'heading':
|
|
183
|
+
return { type: 'heading', depth: n.attrs?.level || 1, children: inlineToMdast(n.content) };
|
|
184
|
+
case 'paragraph':
|
|
185
|
+
return { type: 'paragraph', children: inlineToMdast(n.content) };
|
|
186
|
+
case 'blockquote':
|
|
187
|
+
return { type: 'blockquote', children: blocksToMdast(n.content) };
|
|
188
|
+
case 'bulletList':
|
|
189
|
+
return { type: 'list', ordered: false, spread: false, children: (n.content || []).map(li => ({ type: 'listItem', spread: false, children: blocksToMdast(li.content) })) };
|
|
190
|
+
case 'orderedList':
|
|
191
|
+
return { type: 'list', ordered: true, start: n.attrs?.start || 1, spread: false, children: (n.content || []).map(li => ({ type: 'listItem', spread: false, children: blocksToMdast(li.content) })) };
|
|
192
|
+
case 'taskList':
|
|
193
|
+
return { type: 'list', ordered: false, spread: false, children: (n.content || []).map(li => ({ type: 'listItem', checked: !!li.attrs?.checked, spread: false, children: blocksToMdast(li.content) })) };
|
|
194
|
+
case 'codeBlock':
|
|
195
|
+
return { type: 'code', lang: n.attrs?.language || null, value: (n.content || []).map(c => c.text || '').join('') };
|
|
196
|
+
case 'horizontalRule':
|
|
197
|
+
return { type: 'thematicBreak' };
|
|
198
|
+
case 'table':
|
|
199
|
+
return tableToMdast(n);
|
|
200
|
+
default:
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function tableToMdast(n) {
|
|
206
|
+
return {
|
|
207
|
+
type: 'table',
|
|
208
|
+
align: [],
|
|
209
|
+
children: (n.content || []).map(row => ({
|
|
210
|
+
type: 'tableRow',
|
|
211
|
+
children: (row.content || []).map(cell => ({
|
|
212
|
+
type: 'tableCell',
|
|
213
|
+
children: inlineToMdast((cell.content?.[0] || {}).content),
|
|
214
|
+
})),
|
|
215
|
+
})),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function blocksToMdast(nodes) {
|
|
220
|
+
const out = [];
|
|
221
|
+
for (const n of nodes || []) {
|
|
222
|
+
const b = blockToMdast(n);
|
|
223
|
+
if (b) out.push(b);
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function docToMd(doc, frontmatter = '') {
|
|
229
|
+
const tree = { type: 'root', children: blocksToMdast(doc.content) };
|
|
230
|
+
let md = toMarkdown(tree, { extensions: [gfmToMarkdown()], bullet: '-', emphasis: '_', rule: '-', fences: true });
|
|
231
|
+
if (frontmatter) md = frontmatter.replace(/\n?$/, '\n') + '\n' + md;
|
|
232
|
+
return md;
|
|
233
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Markdown-aware paste for the Tiptap surface, as a ProseMirror plugin.
|
|
2
|
+
//
|
|
3
|
+
// ProseMirror's default paste treats clipboard *plain text* literally: pasting a
|
|
4
|
+
// block of Markdown (from a terminal, another editor, an LLM answer) lands as raw
|
|
5
|
+
// "## Heading" text instead of a real heading. Since our on-disk format IS Markdown
|
|
6
|
+
// and we already own the remark<->PM bridge (md.mjs), we route a plain-text paste
|
|
7
|
+
// through mdToDoc so it becomes real blocks — the same conversion the editor uses
|
|
8
|
+
// on load. See md.mjs.
|
|
9
|
+
//
|
|
10
|
+
// Guardrails (so we never surprise the writer):
|
|
11
|
+
// - a rich source (text/html present) is left to ProseMirror's own handling;
|
|
12
|
+
// - a paste inside a code block stays literal;
|
|
13
|
+
// - a single line with no block-Markdown token is left to the default inline
|
|
14
|
+
// paste (so pasting a URL/word mid-sentence still inserts inline, not as a
|
|
15
|
+
// new paragraph).
|
|
16
|
+
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
17
|
+
import { mdToDoc } from './md.mjs';
|
|
18
|
+
|
|
19
|
+
export const pasteMdKey = new PluginKey('sonorance-md-paste');
|
|
20
|
+
|
|
21
|
+
// A single line only warrants block conversion if it opens with a block-level
|
|
22
|
+
// Markdown token (heading, list, quote, fence, table row, rule).
|
|
23
|
+
const BLOCKISH = /^\s{0,3}(#{1,6}\s|[-*+]\s|\d+[.)]\s|>\s|```|~~~|\||-{3,}\s*$|\*{3,}\s*$|_{3,}\s*$)/;
|
|
24
|
+
|
|
25
|
+
export function markdownPastePlugin() {
|
|
26
|
+
return new Plugin({
|
|
27
|
+
key: pasteMdKey,
|
|
28
|
+
props: {
|
|
29
|
+
handlePaste(view, event) {
|
|
30
|
+
const cd = event.clipboardData;
|
|
31
|
+
if (!cd) return false;
|
|
32
|
+
const text = cd.getData('text/plain');
|
|
33
|
+
if (!text || !text.trim()) return false;
|
|
34
|
+
const html = cd.getData('text/html');
|
|
35
|
+
if (html && html.trim()) return false; // rich source → let PM map it
|
|
36
|
+
const { $from } = view.state.selection;
|
|
37
|
+
if ($from.parent.type.spec.code) return false; // inside a code block → literal
|
|
38
|
+
const multiline = /\r?\n/.test(text.trim());
|
|
39
|
+
if (!multiline && !BLOCKISH.test(text)) return false; // plain inline text → default paste
|
|
40
|
+
|
|
41
|
+
let doc;
|
|
42
|
+
try { ({ doc } = mdToDoc(text)); } catch { return false; }
|
|
43
|
+
const nodes = (doc && doc.content) || [];
|
|
44
|
+
if (!nodes.length) return false;
|
|
45
|
+
|
|
46
|
+
let pmDoc;
|
|
47
|
+
try { pmDoc = view.state.schema.nodeFromJSON(doc); } catch { return false; }
|
|
48
|
+
const slice = pmDoc.slice(0);
|
|
49
|
+
view.dispatch(view.state.tr.replaceSelection(slice).scrollIntoView());
|
|
50
|
+
return true;
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|