@sutrata/editor 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -0
- package/dist/ai/generated-providers.json +1 -1
- package/package.json +30 -3
- package/scripts/copy-assets.mjs +0 -16
- package/scripts/generate-providers.mjs +0 -127
- package/src/ai/OnboardingDialog.tsx +0 -390
- package/src/ai/VoiceConfirmDialog.tsx +0 -137
- package/src/ai/VoiceToolbar.tsx +0 -321
- package/src/ai/ai-client.ts +0 -596
- package/src/ai/commands.ts +0 -164
- package/src/ai/generated-providers.json +0 -3696
- package/src/ai/prompts.ts +0 -62
- package/src/ai/voice-service.ts +0 -96
- package/src/autocomplete/AutocompletePlugin.ts +0 -21
- package/src/autocomplete/suggestions.ts +0 -147
- package/src/context/DocumentContext.tsx +0 -648
- package/src/desktop-api.d.ts +0 -40
- package/src/editor/EditorView.tsx +0 -82
- package/src/editor/SelectionMenu.tsx +0 -152
- package/src/editor/attr-pencil-plugin.ts +0 -172
- package/src/editor/character-decoration-plugin.ts +0 -104
- package/src/editor/editor-bus.ts +0 -51
- package/src/editor/find-highlight-plugin.ts +0 -68
- package/src/editor/frontmatter-field-view.ts +0 -52
- package/src/editor/frontmatter-field.ts +0 -115
- package/src/editor/grapheme-cursor.ts +0 -99
- package/src/editor/ime-plugin.ts +0 -85
- package/src/editor/insert-helper.ts +0 -43
- package/src/editor/keymap.ts +0 -124
- package/src/editor/language-class-plugin.ts +0 -91
- package/src/editor/plugins.ts +0 -26
- package/src/editor/prosemirror-to-sutra.ts +0 -192
- package/src/editor/scene-heading-view.ts +0 -147
- package/src/editor/schema.ts +0 -156
- package/src/editor/sutra-to-prosemirror.ts +0 -156
- package/src/extensions/storage-adapter.ts +0 -65
- package/src/extensions/types.ts +0 -60
- package/src/file/docx-exporter.ts +0 -585
- package/src/file/docx-importer.ts +0 -313
- package/src/file/print-paginator.ts +0 -301
- package/src/file/templates.ts +0 -73
- package/src/file/workflow-reports.ts +0 -820
- package/src/findreplace/FindReplace.tsx +0 -242
- package/src/findreplace/find-engine.ts +0 -97
- package/src/findreplace/scroll-to-match.ts +0 -89
- package/src/fonts/tofu-detect.ts +0 -53
- package/src/i18n/LanguageContext.tsx +0 -207
- package/src/i18n/language-detect.ts +0 -73
- package/src/i18n/useTranslation.tsx +0 -74
- package/src/index.ts +0 -27
- package/src/locales/bn.json +0 -143
- package/src/locales/en.json +0 -143
- package/src/locales/gu.json +0 -143
- package/src/locales/hi.json +0 -143
- package/src/locales/kn.json +0 -143
- package/src/locales/ml.json +0 -143
- package/src/locales/mr.json +0 -143
- package/src/locales/or.json +0 -143
- package/src/locales/pa.json +0 -143
- package/src/locales/si.json +0 -143
- package/src/locales/ta.json +0 -143
- package/src/locales/te.json +0 -143
- package/src/navigator/SceneNavigator.tsx +0 -844
- package/src/navigator/StatisticsDialog.tsx +0 -249
- package/src/navigator/scene-list.ts +0 -84
- package/src/settings/SettingsDialog.tsx +0 -303
- package/src/shell/AppBar.tsx +0 -354
- package/src/shell/AppShell.tsx +0 -71
- package/src/shell/ConfirmDialog.tsx +0 -71
- package/src/shell/ElementToolbar.tsx +0 -175
- package/src/shell/LanguageStatus.tsx +0 -101
- package/src/shell/StatusBar.tsx +0 -135
- package/src/shell/SutrataLogo.tsx +0 -89
- package/src/shell/Toast.tsx +0 -46
- package/src/shell/ToolbarButton.tsx +0 -30
- package/src/shell/VersionHistory.tsx +0 -212
- package/src/shell/Watermark.tsx +0 -9
- package/src/shell/fonts/fonts.css +0 -1133
- package/src/shell/icons.tsx +0 -335
- package/src/shell/toolbar-actions.ts +0 -59
- package/src/source/SourceView.tsx +0 -111
- package/src/source/find-highlight-source.ts +0 -57
- package/src/source/sutra-lang.ts +0 -121
- package/src/spellcheck/dictionaries.ts +0 -21
- package/src/spellcheck/hunspell.ts +0 -110
- package/src/spellcheck/spellcheck-plugin.ts +0 -83
- package/src/styles/StyleDialog.tsx +0 -148
- package/src/styles/builtin-styles.ts +0 -139
- package/src/styles/css-adapter.ts +0 -76
- package/src/styles/docx-adapter.ts +0 -109
- package/src/styles/embed-fonts.ts +0 -81
- package/src/styles/pagination.ts +0 -27
- package/src/styles/print-adapter.ts +0 -288
- package/src/styles/registry.ts +0 -20
- package/src/styles/screenplay.css +0 -3006
- package/src/styles/script-ranges.ts +0 -17
- package/src/styles/types.ts +0 -104
- package/src/styles/validate.ts +0 -114
- package/src/templates/templates.ts +0 -194
- package/src/titlepage/ExportDialog.tsx +0 -494
- package/src/titlepage/TitlePageDialog.tsx +0 -312
- package/src/titlepage/frontmatter-form.ts +0 -280
- package/src/titlepage/languages.ts +0 -37
- package/src/types.ts +0 -7
- package/tests/AppBar.test.tsx +0 -35
- package/tests/AppShell.test.tsx +0 -22
- package/tests/ElementToolbar.test.tsx +0 -36
- package/tests/LanguageStatus.test.tsx +0 -129
- package/tests/SelectionMenu.test.tsx +0 -71
- package/tests/ToolbarButton.test.tsx +0 -24
- package/tests/ai-onboarding.test.ts +0 -102
- package/tests/ai-status.test.tsx +0 -72
- package/tests/ai-structured.test.ts +0 -94
- package/tests/autocomplete/AutocompletePlugin.test.ts +0 -20
- package/tests/autocomplete/suggestions.test.ts +0 -106
- package/tests/document-context.test.tsx +0 -565
- package/tests/docx-importer.test.ts +0 -160
- package/tests/editor/character-decoration-plugin.test.ts +0 -101
- package/tests/editor/character-extension.test.ts +0 -44
- package/tests/editor/grapheme-cursor.test.ts +0 -118
- package/tests/editor/ime-plugin.test.ts +0 -72
- package/tests/editor/inline-note-bridge.test.ts +0 -32
- package/tests/editor/language-class-plugin.test.ts +0 -265
- package/tests/editor/title-page.test.ts +0 -124
- package/tests/embed-fonts.test.ts +0 -17
- package/tests/exporters.test.ts +0 -365
- package/tests/findreplace/FindReplace.test.tsx +0 -391
- package/tests/findreplace/find-engine.test.ts +0 -86
- package/tests/findreplace/scroll-to-match.test.ts +0 -50
- package/tests/fonts/tofu-detect.test.ts +0 -122
- package/tests/i18n/LanguageContext.test.tsx +0 -146
- package/tests/i18n/language-detect.test.ts +0 -54
- package/tests/i18n/locale-parity.test.ts +0 -25
- package/tests/i18n/useTranslation.test.tsx +0 -45
- package/tests/keymap.test.ts +0 -18
- package/tests/navigator/SceneNavigator.test.tsx +0 -177
- package/tests/navigator/StatisticsDialog.test.tsx +0 -63
- package/tests/navigator/scene-list.test.ts +0 -71
- package/tests/print-paginator.test.ts +0 -82
- package/tests/shell/VersionHistory.test.tsx +0 -129
- package/tests/spellcheck/hunspell.test.ts +0 -82
- package/tests/spellcheck/spellcheck-plugin.test.ts +0 -56
- package/tests/styles.test.ts +0 -315
- package/tests/templates/templates.test.ts +0 -83
- package/tests/titlepage/TitlePageDialog.test.tsx +0 -90
- package/tests/titlepage/apply-title-page.test.ts +0 -61
- package/tests/titlepage/frontmatter-form.test.ts +0 -115
- package/tests/toolbar-actions.test.ts +0 -93
- package/tests/voice-commands.test.ts +0 -58
- package/tsconfig.json +0 -17
- package/vitest.config.ts +0 -11
- package/vitest.setup.ts +0 -65
package/src/ai/prompts.ts
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
export const VOICE_FORMAT_SYSTEM_PROMPT = `You are Sutrata's AI screenplay formatter.
|
|
2
|
-
Your task is to take a raw voice dictation transcript (which may contain explicit formatting keywords like "scene", "character", "dialogue", "parenthetical", "transition") and convert it into valid Sutra screenplay format.
|
|
3
|
-
|
|
4
|
-
Sutra syntax rules:
|
|
5
|
-
1. Scene Headings: Start with "##" (e.g., "## INT. COFFEE SHOP - DAY"). If a scene number/ID is specified (e.g., "scene #2", "scene 2", or "scene 3A"), append it as a Sutra attribute ID at the end of the scene heading, like "{#2}" or "{#3A}" (e.g., "## EXT. SWIMMING POOL - DAY {#2}").
|
|
6
|
-
2. Characters: All-caps, alone on a line (e.g., "RAJ").
|
|
7
|
-
3. Parentheticals: In parentheses, on a line by itself under character (e.g., "(whispering)").
|
|
8
|
-
4. Dialogue: Indented text directly following a character or parenthetical.
|
|
9
|
-
5. Transitions: In uppercase, prefixed with ">" or ending with "TO:" (e.g., "> CUT TO:").
|
|
10
|
-
6. Lyrics: Prefixed with "~" (e.g., "~ Sing a song").
|
|
11
|
-
7. Action: Standard descriptive paragraph.
|
|
12
|
-
|
|
13
|
-
CRITICAL INSTRUCTIONS:
|
|
14
|
-
- Preserve Indic/Unicode characters and mixed-language code-switching exactly as transcribed. Do NOT translate non-English text to English.
|
|
15
|
-
- Output ONLY the formatted Sutra text.
|
|
16
|
-
- Do NOT include any explanations, steps, reasoning, bullet points, meta-commentary, or introductory/concluding text.
|
|
17
|
-
- Do NOT output headings, drafts, prefixes, or suffixes.
|
|
18
|
-
- Do NOT wrap in markdown code blocks like \`\`\`cine.
|
|
19
|
-
- The output must contain nothing but the final formatted Sutra screenplay lines.
|
|
20
|
-
`;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Combined synopsis+duration prompt. Merged into one call (and one JSON
|
|
24
|
-
* response) instead of two separate requests to halve per-scene LLM cost.
|
|
25
|
-
* Paired with SCENE_METADATA_SCHEMA — the response format instructions
|
|
26
|
-
* (JSON-only, matching the schema) are appended by callAIStructured, not
|
|
27
|
-
* hardcoded here, so the same prompt works across providers.
|
|
28
|
-
*/
|
|
29
|
-
export const SCENE_METADATA_PROMPT = `[INSTRUCTION]
|
|
30
|
-
Analyze the screenplay scene provided below and produce two fields:
|
|
31
|
-
1. "synopsis": A concise, 1-2 sentence synopsis of the main action and conflict.
|
|
32
|
-
2. "duration": The estimated onscreen duration of the scene, in "MM:SS" format (e.g. 01:30).
|
|
33
|
-
|
|
34
|
-
[RULES FOR "synopsis"]
|
|
35
|
-
1. Match the language of the scene: If the scene is in Tamil, the synopsis MUST be written in Tamil. If the scene is in Hindi, the synopsis MUST be written in Hindi, etc. Do NOT translate to English.
|
|
36
|
-
2. Do NOT include any explanations, definitions, drafts, translations, or bullet points.
|
|
37
|
-
3. Do NOT include headings, prefixes (such as "Synopsis:" or "Draft:"), or suffixes.
|
|
38
|
-
4. Absolutely no English introduction, reasoning, or meta-commentary.
|
|
39
|
-
|
|
40
|
-
[RULES FOR "duration"]
|
|
41
|
-
1. Dialogue speed: ~140 words per minute.
|
|
42
|
-
2. Action lines: ~4 seconds per line of description.
|
|
43
|
-
3. The value must be ONLY the estimated duration in "MM:SS" format — no calculations, explanations, breakdown, or notes.
|
|
44
|
-
`;
|
|
45
|
-
|
|
46
|
-
/** JSON schema for SCENE_METADATA_PROMPT's response — see callAIStructured. */
|
|
47
|
-
export const SCENE_METADATA_SCHEMA = {
|
|
48
|
-
type: 'object',
|
|
49
|
-
properties: {
|
|
50
|
-
synopsis: {
|
|
51
|
-
type: 'string',
|
|
52
|
-
description: "1-2 sentence synopsis of the scene's main action and conflict, in the scene's own language/script",
|
|
53
|
-
},
|
|
54
|
-
duration: {
|
|
55
|
-
type: 'string',
|
|
56
|
-
description: 'Estimated onscreen duration of the scene as MM:SS',
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
required: ['synopsis', 'duration'],
|
|
60
|
-
additionalProperties: false,
|
|
61
|
-
} as const;
|
|
62
|
-
|
package/src/ai/voice-service.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
export interface VoiceServiceCallbacks {
|
|
2
|
-
onResult: (text: string, isFinal: boolean) => void;
|
|
3
|
-
onError: (error: string) => void;
|
|
4
|
-
onEnd: () => void;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export class VoiceService {
|
|
8
|
-
private recognition: any = null;
|
|
9
|
-
private isListening: boolean = false;
|
|
10
|
-
|
|
11
|
-
constructor() {
|
|
12
|
-
const SpeechRecognition =
|
|
13
|
-
(window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
|
|
14
|
-
if (SpeechRecognition) {
|
|
15
|
-
this.recognition = new SpeechRecognition();
|
|
16
|
-
this.recognition.continuous = true;
|
|
17
|
-
this.recognition.interimResults = true;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
public isSupported(): boolean {
|
|
22
|
-
return !!this.recognition;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
public start(lang: string, callbacks: VoiceServiceCallbacks) {
|
|
26
|
-
if (!this.recognition) {
|
|
27
|
-
callbacks.onError('Web Speech API is not supported in this browser.');
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
if (this.isListening) {
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
this.recognition.lang = lang;
|
|
35
|
-
this.isListening = true;
|
|
36
|
-
|
|
37
|
-
this.recognition.onresult = (event: any) => {
|
|
38
|
-
let interimTranscript = '';
|
|
39
|
-
let finalTranscript = '';
|
|
40
|
-
|
|
41
|
-
for (let i = event.resultIndex; i < event.results.length; ++i) {
|
|
42
|
-
if (event.results[i].isFinal) {
|
|
43
|
-
finalTranscript += event.results[i][0].transcript;
|
|
44
|
-
} else {
|
|
45
|
-
interimTranscript += event.results[i][0].transcript;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const text = finalTranscript || interimTranscript;
|
|
50
|
-
callbacks.onResult(text, finalTranscript.length > 0);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
this.recognition.onerror = (event: any) => {
|
|
54
|
-
// Ignore 'no-speech' error to prevent crashing, just log it
|
|
55
|
-
if (event.error === 'no-speech') {
|
|
56
|
-
console.warn('Speech recognition: no speech detected');
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
callbacks.onError(event.error || 'Speech recognition error');
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
this.recognition.onend = () => {
|
|
63
|
-
this.isListening = false;
|
|
64
|
-
callbacks.onEnd();
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
try {
|
|
68
|
-
this.recognition.start();
|
|
69
|
-
} catch (e: any) {
|
|
70
|
-
callbacks.onError(e.message || String(e));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
public stop() {
|
|
75
|
-
if (this.recognition && this.isListening) {
|
|
76
|
-
try {
|
|
77
|
-
this.recognition.stop();
|
|
78
|
-
} catch (e) {
|
|
79
|
-
console.error('Failed to stop recognition:', e);
|
|
80
|
-
}
|
|
81
|
-
this.isListening = false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
public abort() {
|
|
86
|
-
if (this.recognition && this.isListening) {
|
|
87
|
-
try {
|
|
88
|
-
this.recognition.abort();
|
|
89
|
-
} catch (e) {
|
|
90
|
-
console.error('Failed to abort recognition:', e);
|
|
91
|
-
}
|
|
92
|
-
this.isListening = false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
export const voiceServiceInstance = new VoiceService();
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { Plugin } from 'prosemirror-state'
|
|
2
|
-
import { EditorView } from 'prosemirror-view'
|
|
3
|
-
|
|
4
|
-
// Character-cue autocomplete was disabled: the dropdown interrupted typing
|
|
5
|
-
// more than it helped. This plugin is kept as a no-op registration point
|
|
6
|
-
// in case popup-based suggestions are revisited for other cue types.
|
|
7
|
-
export function createAutocompletePlugin() {
|
|
8
|
-
return new Plugin({
|
|
9
|
-
view(_editorView: EditorView) {
|
|
10
|
-
return {
|
|
11
|
-
update(_view: EditorView) {},
|
|
12
|
-
destroy() {},
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
props: {
|
|
16
|
-
handleKeyDown(_view: EditorView, _event: KeyboardEvent): boolean {
|
|
17
|
-
return false
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
})
|
|
21
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
export interface Candidate {
|
|
2
|
-
label: string
|
|
3
|
-
type: 'character' | 'location' | 'transition' | 'metadata_key'
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const PREDEFINED_TRANSITIONS = [
|
|
7
|
-
'CUT TO:', 'FADE IN:', 'FADE OUT:', 'DISSOLVE TO:', 'SMASH CUT TO:',
|
|
8
|
-
'MATCH CUT TO:', 'WIPE TO:', 'IRIS IN:', 'IRIS OUT:',
|
|
9
|
-
]
|
|
10
|
-
|
|
11
|
-
const RESERVED_METADATA_KEYS = [
|
|
12
|
-
'status', 'tags', 'location', 'time', 'number', 'lang', 'mood',
|
|
13
|
-
'arc', 'note', 'locked',
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Extract character candidates from screenplay text.
|
|
18
|
-
* Sources: @CHARACTER cues in the text and {#characters} registry blocks.
|
|
19
|
-
*/
|
|
20
|
-
export function buildCharacterCandidates(text: string): Candidate[] {
|
|
21
|
-
const seen = new Set<string>()
|
|
22
|
-
const candidates: Candidate[] = []
|
|
23
|
-
|
|
24
|
-
// Scan for @CHARACTER lines (character block sigil)
|
|
25
|
-
const lines = text.split('\n')
|
|
26
|
-
for (const line of lines) {
|
|
27
|
-
const trimmed = line.trim()
|
|
28
|
-
if (trimmed.startsWith('@')) {
|
|
29
|
-
const name = trimmed.slice(1).trim().toUpperCase()
|
|
30
|
-
if (name && !seen.has(name)) {
|
|
31
|
-
seen.add(name)
|
|
32
|
-
candidates.push({ label: name, type: 'character' })
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Also scan for names in {#characters} registry blocks
|
|
38
|
-
const charRegistryRegex = /\{#characters\}([\s\S]*?)\{\/characters\}/g
|
|
39
|
-
let match
|
|
40
|
-
while ((match = charRegistryRegex.exec(text)) !== null) {
|
|
41
|
-
const block = match[1]
|
|
42
|
-
if (block) {
|
|
43
|
-
for (const line of block.split('\n')) {
|
|
44
|
-
const name = line.trim().toUpperCase()
|
|
45
|
-
if (name && !seen.has(name)) {
|
|
46
|
-
seen.add(name)
|
|
47
|
-
candidates.push({ label: name, type: 'character' })
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return candidates.sort((a, b) => a.label.localeCompare(b.label))
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Extract location candidates from scene headings and & location: metadata.
|
|
58
|
-
*/
|
|
59
|
-
export function buildLocationCandidates(text: string): Candidate[] {
|
|
60
|
-
const seen = new Set<string>()
|
|
61
|
-
const candidates: Candidate[] = []
|
|
62
|
-
|
|
63
|
-
const lines = text.split('\n')
|
|
64
|
-
for (const line of lines) {
|
|
65
|
-
const trimmed = line.trim()
|
|
66
|
-
|
|
67
|
-
// Scene headings: ## INT. LIVING ROOM - DAY → location = "INT. LIVING ROOM"
|
|
68
|
-
if (trimmed.startsWith('## ')) {
|
|
69
|
-
const heading = trimmed.slice(3).trim()
|
|
70
|
-
// Extract location part (before the dash)
|
|
71
|
-
const dashIdx = heading.indexOf(' - ')
|
|
72
|
-
const loc = dashIdx >= 0 ? heading.slice(0, dashIdx).trim() : heading
|
|
73
|
-
if (loc && !seen.has(loc)) {
|
|
74
|
-
seen.add(loc)
|
|
75
|
-
candidates.push({ label: loc, type: 'location' })
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// & location: metadata
|
|
80
|
-
if (trimmed.startsWith('& location:')) {
|
|
81
|
-
const loc = trimmed.replace('& location:', '').trim()
|
|
82
|
-
if (loc && !seen.has(loc)) {
|
|
83
|
-
seen.add(loc)
|
|
84
|
-
candidates.push({ label: loc, type: 'location' })
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return candidates.sort((a, b) => a.label.localeCompare(b.label))
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Build transition candidates: predefined + used in text.
|
|
94
|
-
*/
|
|
95
|
-
export function buildTransitionCandidates(text: string): Candidate[] {
|
|
96
|
-
const seen = new Set<string>(PREDEFINED_TRANSITIONS)
|
|
97
|
-
const candidates: Candidate[] = PREDEFINED_TRANSITIONS.map(t => ({ label: t, type: 'transition' as const }))
|
|
98
|
-
|
|
99
|
-
const lines = text.split('\n')
|
|
100
|
-
for (const line of lines) {
|
|
101
|
-
const trimmed = line.trim()
|
|
102
|
-
// Transition lines start with >>
|
|
103
|
-
if (trimmed.startsWith('>> ')) {
|
|
104
|
-
const trans = trimmed.slice(3).trim()
|
|
105
|
-
if (trans && !seen.has(trans)) {
|
|
106
|
-
seen.add(trans)
|
|
107
|
-
candidates.push({ label: trans, type: 'transition' })
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return candidates
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Build metadata key candidates: reserved + used in text.
|
|
117
|
-
*/
|
|
118
|
-
export function buildMetadataKeyCandidates(text: string): Candidate[] {
|
|
119
|
-
const seen = new Set<string>(RESERVED_METADATA_KEYS)
|
|
120
|
-
const candidates: Candidate[] = RESERVED_METADATA_KEYS.map(k => ({ label: k, type: 'metadata_key' as const }))
|
|
121
|
-
|
|
122
|
-
const lines = text.split('\n')
|
|
123
|
-
for (const line of lines) {
|
|
124
|
-
const trimmed = line.trim()
|
|
125
|
-
if (trimmed.startsWith('& ')) {
|
|
126
|
-
const keyMatch = /^& ([a-zA-Z_]+):/.exec(trimmed)
|
|
127
|
-
if (keyMatch) {
|
|
128
|
-
const key = keyMatch[1]
|
|
129
|
-
if (key && !seen.has(key)) {
|
|
130
|
-
seen.add(key)
|
|
131
|
-
candidates.push({ label: key, type: 'metadata_key' })
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return candidates
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Filter candidates by prefix (case-insensitive).
|
|
142
|
-
*/
|
|
143
|
-
export function filterCandidates(candidates: Candidate[], prefix: string): Candidate[] {
|
|
144
|
-
if (!prefix) return candidates
|
|
145
|
-
const lower = prefix.toLowerCase()
|
|
146
|
-
return candidates.filter(c => c.label.toLowerCase().startsWith(lower))
|
|
147
|
-
}
|