@notegen/plugin-api 0.1.0 → 0.1.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/README.md +88 -0
- package/dist/index.d.ts +130 -5
- package/dist/index.js +148 -1
- package/package.json +2 -2
- package/schema/plugin-manifest-v1.schema.json +6 -2
package/README.md
CHANGED
|
@@ -100,3 +100,91 @@ to the old data branch. Reinstalling an existing fingerprint reuses its branch.
|
|
|
100
100
|
This does not roll back note writes, attachments, settings or remote effects.
|
|
101
101
|
Use explicit data schema versions and idempotent migration steps. The test host
|
|
102
102
|
does not simulate package installation or versioned storage.
|
|
103
|
+
|
|
104
|
+
## Active document paths
|
|
105
|
+
|
|
106
|
+
`editor.getActiveEditor()` and active-editor events may include `path`, the workspace-relative Markdown path. It is protected by `editor.read` and never contains an absolute filesystem path. Older hosts and non-workspace documents may omit it; plugins must handle absence without treating the opaque document ID as a path.
|
|
107
|
+
|
|
108
|
+
## Opening existing notes only
|
|
109
|
+
|
|
110
|
+
Updated hosts accept `notes.openOrCreate({ ..., open: true, create: false })`. This requires `notes.open` only and fails for missing files without creating them. Omitted `create` preserves the original behavior and requires `notes.create`. Open-only mode requires an updated host; older hosts do not implement this option.
|
|
111
|
+
|
|
112
|
+
## Navigation lists (updated local hosts)
|
|
113
|
+
|
|
114
|
+
The `navigation-list` UI block provides a compact host-rendered sortable list. Each item has a unique stable ID and a label. Item open/remove commands receive `{ generation, itemId }`; the add command receives `{ generation }`. Reorder receives `{ generation, itemIds }` with the complete new order. Plugins must check generation, reject duplicate/missing/foreign IDs, persist the new order, and publish a new document. Hosts validate all referenced commands and limit lists to 100 items. Older hosts reject this block type.
|
|
115
|
+
|
|
116
|
+
## API 0.1.1: editor menus and composable UI
|
|
117
|
+
|
|
118
|
+
New contributions require `apiVersion: "^0.1.1"`. The corresponding NoteGen host
|
|
119
|
+
changes must be present; publishing this SDK does not update an installed app.
|
|
120
|
+
The older `^0.1.0` plugin contract remains accepted by the new host.
|
|
121
|
+
|
|
122
|
+
Menu locations now include `editor/slash`, `editor/context`, `editor/selection`
|
|
123
|
+
(the text-selection floating toolbar), `editor/toolbar` (visual editor footer),
|
|
124
|
+
`tab/context`, `file/context`, and `mobile/writing/overflow`.
|
|
125
|
+
`editor/context` uses Alt/Option + right-click to preserve the native clipboard menu.
|
|
126
|
+
The tab menu also retains file-menu contributions, deduplicated by command ID.
|
|
127
|
+
|
|
128
|
+
Each menu accepts `icon`, `group`, `order`, `when`, and `enableWhen`. Groups sort
|
|
129
|
+
lexically, then entries sort by ascending order. A menu icon overrides the command
|
|
130
|
+
icon. Commands accept up to 20 `keywords` for slash-menu and palette search.
|
|
131
|
+
Selection/toolbar menus display three direct buttons and put additional commands
|
|
132
|
+
in an accessible overflow menu. Existing built-in editing commands remain intact.
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"location": "editor/selection",
|
|
137
|
+
"command": "com.example.notes.extract",
|
|
138
|
+
"icon": "files",
|
|
139
|
+
"group": "notes",
|
|
140
|
+
"order": 10,
|
|
141
|
+
"when": "editor == markdown && selection",
|
|
142
|
+
"enableWhen": "!readOnly && !codeBlock"
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Conditions support boolean `selection`, `readOnly`, `codeBlock`; string `editor`,
|
|
147
|
+
`resourceKind` (`file`, `folder`, `root`), and `resourceExt` (e.g. `md`). Use `!`,
|
|
148
|
+
`==`, `!=`, `&&`, `||`; `&&` binds more tightly than `||`. Parentheses, arbitrary
|
|
149
|
+
properties and executable JavaScript are rejected. Unknown/missing context fails
|
|
150
|
+
closed, including negated conditions. Conditions control UI only, never permission
|
|
151
|
+
grants. Read selection/text through `editor.read`; keep the captured editor ID and
|
|
152
|
+
revision when applying an edit through `editor.write`. Menu arguments do not leak
|
|
153
|
+
selected text or circumvent permissions.
|
|
154
|
+
|
|
155
|
+
Composable blocks:
|
|
156
|
+
|
|
157
|
+
- `layout`: row/column, small/medium/large gap, nested `blocks`.
|
|
158
|
+
- `section`: title, nested blocks, optional collapse and initial open state.
|
|
159
|
+
- `tabs`: stable ID, accessible label, tabs with ID/label/blocks.
|
|
160
|
+
- `toolbar`: labeled actions with optional icon, iconOnly, variant and confirmation.
|
|
161
|
+
- `item-list`: stable IDs, generation, click action, optional checkbox action,
|
|
162
|
+
drag and keyboard reordering, context actions and a touch-accessible overflow.
|
|
163
|
+
- `markdown`: formatted text with raw HTML, links and image loading disabled.
|
|
164
|
+
- `badge`, `empty`, `loading`: standard theme-aware feedback.
|
|
165
|
+
- Form fields additionally support `search`, ISO `date`, and searchable
|
|
166
|
+
`note-picker`. Supply note choices as `{label,value}` through existing scoped
|
|
167
|
+
`notes.list` permissions. The picker does not enumerate files or grant access.
|
|
168
|
+
|
|
169
|
+
Nested forms retain their values when a surrounding section/tab rerenders. IDs
|
|
170
|
+
must be unique per block type across the whole document. Limits: 6 nesting levels,
|
|
171
|
+
200 total blocks, 50 blocks per container, 100 list items, 20 actions, 12 tabs,
|
|
172
|
+
and the existing 128 KiB document limit. Command ownership is validated recursively.
|
|
173
|
+
|
|
174
|
+
List open/toggle/action commands receive `{generation,itemId}`; toggle adds
|
|
175
|
+
`checked`. Context actions add `actionId` and optionally nested `argument`.
|
|
176
|
+
Reorder commands receive `{generation,itemIds}` with the complete requested order.
|
|
177
|
+
The plugin must reject stale generations and invalid IDs, persist successful
|
|
178
|
+
changes, then publish updated content. The host never mutates plugin storage.
|
|
179
|
+
|
|
180
|
+
An action may provide `confirmation: {title,description?,confirmLabel,cancelLabel}`.
|
|
181
|
+
Its command runs only after confirmation. Legacy `navigation-list` remains a
|
|
182
|
+
compatibility adapter; new plugins should compose toolbar and item-list blocks.
|
|
183
|
+
|
|
184
|
+
Supported symbolic icons include bookmark, calendar-days, file-text, files, folder,
|
|
185
|
+
folder-open, layout-template, list-checks, list-todo, link, search, plus, minus,
|
|
186
|
+
trash-2, pencil, copy, check, x, star, pin, tag, settings, more-horizontal, arrow-up,
|
|
187
|
+
arrow-down, download, upload, external-link, list, table-2, columns-3, clock,
|
|
188
|
+
book-open, code, sparkles, shuffle, refresh-cw, chart-no-axes-combined, file-input,
|
|
189
|
+
and flask-conical. Unknown icons fall back to a puzzle icon. Raw SVG/HTML and
|
|
190
|
+
external icon URLs are not accepted as executable markup.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The public API version implemented by this release of NoteGen. */
|
|
2
|
-
export declare const PLUGIN_API_VERSION: "0.1.
|
|
2
|
+
export declare const PLUGIN_API_VERSION: "0.1.1";
|
|
3
3
|
export type PluginPlatform = 'desktop' | 'ios' | 'android';
|
|
4
4
|
export type PluginActivationEvent = `onCommand:${string}` | 'onEditor:markdown' | 'onWorkspace:open' | 'onNotes:change';
|
|
5
5
|
export type PluginPermissionScope = 'active-editor' | 'workspace-file' | 'workspace-files' | 'workspace-folder' | 'network-origins';
|
|
@@ -28,6 +28,7 @@ export interface PluginCommandContribution {
|
|
|
28
28
|
title: string;
|
|
29
29
|
description?: string;
|
|
30
30
|
icon?: string;
|
|
31
|
+
keywords?: readonly string[];
|
|
31
32
|
suggestedShortcut?: string;
|
|
32
33
|
}
|
|
33
34
|
export interface PluginSettingOption {
|
|
@@ -91,12 +92,15 @@ export interface PluginViewContribution {
|
|
|
91
92
|
location: 'left-sidebar' | 'right-sidebar' | 'editor-tab';
|
|
92
93
|
icon?: string;
|
|
93
94
|
}
|
|
94
|
-
export type PluginMenuLocation = 'editor/slash' | 'editor/context' | 'file/context' | 'mobile/writing/overflow';
|
|
95
|
+
export type PluginMenuLocation = 'editor/slash' | 'editor/context' | 'editor/selection' | 'editor/toolbar' | 'tab/context' | 'file/context' | 'mobile/writing/overflow';
|
|
95
96
|
export interface PluginMenuContribution {
|
|
96
97
|
location: PluginMenuLocation;
|
|
97
98
|
command: string;
|
|
98
99
|
when?: string;
|
|
99
100
|
group?: string;
|
|
101
|
+
order?: number;
|
|
102
|
+
icon?: string;
|
|
103
|
+
enableWhen?: string;
|
|
100
104
|
}
|
|
101
105
|
export interface PluginContributions {
|
|
102
106
|
commands?: readonly PluginCommandContribution[];
|
|
@@ -165,6 +169,8 @@ export interface ReadNoteOptions {
|
|
|
165
169
|
path: string;
|
|
166
170
|
}
|
|
167
171
|
export interface OpenOrCreateNoteOptions {
|
|
172
|
+
/** Defaults to true. Set false with open: true to open an existing file using only notes.open. Missing files fail. */
|
|
173
|
+
create?: boolean;
|
|
168
174
|
workspaceId: string;
|
|
169
175
|
path: string;
|
|
170
176
|
initialContent: string;
|
|
@@ -239,6 +245,8 @@ export interface NoteChangeEvent {
|
|
|
239
245
|
previousPath?: string;
|
|
240
246
|
}
|
|
241
247
|
export interface ActiveEditorContext {
|
|
248
|
+
/** Workspace-relative Markdown path, when available. Never an absolute path. Requires editor.read. */
|
|
249
|
+
path?: string;
|
|
242
250
|
windowId: string;
|
|
243
251
|
editorId: string;
|
|
244
252
|
documentId: string;
|
|
@@ -324,7 +332,7 @@ export type PluginFormField = {
|
|
|
324
332
|
disabled?: boolean;
|
|
325
333
|
visibleWhen?: PluginFormCondition;
|
|
326
334
|
} & ({
|
|
327
|
-
type: 'text' | 'textarea';
|
|
335
|
+
type: 'text' | 'textarea' | 'search' | 'date';
|
|
328
336
|
value?: string;
|
|
329
337
|
placeholder?: string;
|
|
330
338
|
maxLength?: number;
|
|
@@ -334,7 +342,7 @@ export type PluginFormField = {
|
|
|
334
342
|
min?: number;
|
|
335
343
|
max?: number;
|
|
336
344
|
} | {
|
|
337
|
-
type: 'select';
|
|
345
|
+
type: 'select' | 'note-picker';
|
|
338
346
|
value?: string;
|
|
339
347
|
options: readonly {
|
|
340
348
|
label: string;
|
|
@@ -385,7 +393,27 @@ export type PluginTableCell = string | {
|
|
|
385
393
|
argument?: PluginCommandArgument;
|
|
386
394
|
disabled?: boolean;
|
|
387
395
|
};
|
|
388
|
-
|
|
396
|
+
/** Host-rendered navigation list. Commands receive { generation, itemId? };
|
|
397
|
+
* reorder receives { generation, itemIds } containing the complete new order. */
|
|
398
|
+
export interface PluginNavigationListBlock {
|
|
399
|
+
type: 'navigation-list';
|
|
400
|
+
id: string;
|
|
401
|
+
generation: string;
|
|
402
|
+
label: string;
|
|
403
|
+
emptyText: string;
|
|
404
|
+
addLabel: string;
|
|
405
|
+
removeLabel: string;
|
|
406
|
+
reorderLabel: string;
|
|
407
|
+
items: readonly {
|
|
408
|
+
id: string;
|
|
409
|
+
label: string;
|
|
410
|
+
}[];
|
|
411
|
+
openCommand: string;
|
|
412
|
+
addCommand: string;
|
|
413
|
+
removeCommand: string;
|
|
414
|
+
reorderCommand: string;
|
|
415
|
+
}
|
|
416
|
+
export type PluginUiBlock = PluginExtendedUiBlock | PluginNavigationListBlock | PluginFormBlock | {
|
|
389
417
|
type: 'separator';
|
|
390
418
|
} | {
|
|
391
419
|
type: 'callout';
|
|
@@ -615,3 +643,100 @@ export declare class PluginError extends Error {
|
|
|
615
643
|
export declare function isPluginError(value: unknown): value is PluginError;
|
|
616
644
|
/** Preserve literal manifest values while checking the v1 shape at compile time. */
|
|
617
645
|
export declare function definePluginManifest<const Manifest extends PluginManifestV1>(manifest: Manifest): Manifest;
|
|
646
|
+
/** Menu conditions are data, never JavaScript. && binds more tightly than ||.
|
|
647
|
+
* Supported atoms: boolean keys / !key, or key == value / key != value.
|
|
648
|
+
* No parentheses, property access, or executable expressions are accepted. */
|
|
649
|
+
export interface PluginMenuContext {
|
|
650
|
+
editor?: 'markdown';
|
|
651
|
+
selection?: boolean;
|
|
652
|
+
readOnly?: boolean;
|
|
653
|
+
codeBlock?: boolean;
|
|
654
|
+
resourceKind?: 'file' | 'folder' | 'root';
|
|
655
|
+
resourceExt?: string;
|
|
656
|
+
}
|
|
657
|
+
export declare function isValidPluginMenuCondition(condition: string): boolean;
|
|
658
|
+
export declare function matchesPluginMenuCondition(condition: string | undefined, context: PluginMenuContext): boolean;
|
|
659
|
+
export interface PluginActionConfirmation {
|
|
660
|
+
title: string;
|
|
661
|
+
description?: string;
|
|
662
|
+
confirmLabel: string;
|
|
663
|
+
cancelLabel: string;
|
|
664
|
+
}
|
|
665
|
+
export interface PluginUiAction {
|
|
666
|
+
id: string;
|
|
667
|
+
label: string;
|
|
668
|
+
command: string;
|
|
669
|
+
argument?: PluginCommandArgument;
|
|
670
|
+
icon?: string;
|
|
671
|
+
iconOnly?: boolean;
|
|
672
|
+
confirmation?: PluginActionConfirmation;
|
|
673
|
+
disabled?: boolean;
|
|
674
|
+
variant?: 'default' | 'secondary' | 'destructive' | 'ghost' | 'outline';
|
|
675
|
+
}
|
|
676
|
+
export interface PluginItemListBlock {
|
|
677
|
+
type: 'item-list';
|
|
678
|
+
id: string;
|
|
679
|
+
generation: string;
|
|
680
|
+
label: string;
|
|
681
|
+
emptyText: string;
|
|
682
|
+
items: readonly {
|
|
683
|
+
id: string;
|
|
684
|
+
label: string;
|
|
685
|
+
description?: string;
|
|
686
|
+
icon?: string;
|
|
687
|
+
checked?: boolean;
|
|
688
|
+
disabled?: boolean;
|
|
689
|
+
}[];
|
|
690
|
+
openCommand?: string;
|
|
691
|
+
toggleCommand?: string;
|
|
692
|
+
reorderCommand?: string;
|
|
693
|
+
reorderLabel?: string;
|
|
694
|
+
actions?: readonly PluginUiAction[];
|
|
695
|
+
}
|
|
696
|
+
export type PluginExtendedUiBlock = PluginItemListBlock | {
|
|
697
|
+
type: 'layout';
|
|
698
|
+
id: string;
|
|
699
|
+
direction?: 'row' | 'column';
|
|
700
|
+
gap?: 'small' | 'medium' | 'large';
|
|
701
|
+
blocks: readonly PluginUiBlock[];
|
|
702
|
+
} | {
|
|
703
|
+
type: 'section';
|
|
704
|
+
id: string;
|
|
705
|
+
title: string;
|
|
706
|
+
collapsible?: boolean;
|
|
707
|
+
defaultOpen?: boolean;
|
|
708
|
+
blocks: readonly PluginUiBlock[];
|
|
709
|
+
} | {
|
|
710
|
+
type: 'tabs';
|
|
711
|
+
id: string;
|
|
712
|
+
label: string;
|
|
713
|
+
tabs: readonly {
|
|
714
|
+
id: string;
|
|
715
|
+
label: string;
|
|
716
|
+
blocks: readonly PluginUiBlock[];
|
|
717
|
+
}[];
|
|
718
|
+
} | {
|
|
719
|
+
type: 'toolbar';
|
|
720
|
+
id: string;
|
|
721
|
+
label: string;
|
|
722
|
+
actions: readonly PluginUiAction[];
|
|
723
|
+
} | {
|
|
724
|
+
type: 'markdown';
|
|
725
|
+
text: string;
|
|
726
|
+
} | {
|
|
727
|
+
type: 'badge';
|
|
728
|
+
text: string;
|
|
729
|
+
tone?: 'default' | 'secondary' | 'outline' | 'destructive';
|
|
730
|
+
} | {
|
|
731
|
+
type: 'empty';
|
|
732
|
+
title: string;
|
|
733
|
+
description?: string;
|
|
734
|
+
icon?: string;
|
|
735
|
+
} | {
|
|
736
|
+
type: 'loading';
|
|
737
|
+
label: string;
|
|
738
|
+
};
|
|
739
|
+
/** Walk a UI document with shared depth/count limits, including nested forms. */
|
|
740
|
+
export declare function flattenPluginUiBlocks(blocks: readonly PluginUiBlock[]): PluginUiBlock[];
|
|
741
|
+
/** Shared validation used by NoteGen and the SDK's in-memory host. */
|
|
742
|
+
export declare function parsePluginUiExtension(value: unknown, parseChildren: (value: unknown) => readonly PluginUiBlock[]): PluginExtendedUiBlock | undefined;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** The public API version implemented by this release of NoteGen. */
|
|
2
|
-
export const PLUGIN_API_VERSION = '0.1.
|
|
2
|
+
export const PLUGIN_API_VERSION = '0.1.1';
|
|
3
3
|
export const PLUGIN_ERROR_CODES = Object.freeze([
|
|
4
4
|
'PermissionDenied',
|
|
5
5
|
'AlreadyRegistered',
|
|
@@ -45,3 +45,150 @@ export function isPluginError(value) {
|
|
|
45
45
|
export function definePluginManifest(manifest) {
|
|
46
46
|
return manifest;
|
|
47
47
|
}
|
|
48
|
+
const menuBooleanKeys = new Set(['selection', 'readOnly', 'codeBlock']);
|
|
49
|
+
const menuStringKeys = new Set(['editor', 'resourceKind', 'resourceExt']);
|
|
50
|
+
function parseMenuAtom(atom) {
|
|
51
|
+
const boolean = /^(!)?(selection|readOnly|codeBlock)$/.exec(atom.trim());
|
|
52
|
+
if (boolean)
|
|
53
|
+
return { key: boolean[2], operator: '==', value: !boolean[1] };
|
|
54
|
+
const comparison = /^(\w+)\s*(==|!=)\s*([\w.-]+)$/.exec(atom.trim());
|
|
55
|
+
if (!comparison)
|
|
56
|
+
return null;
|
|
57
|
+
const [, key, operator, value] = comparison;
|
|
58
|
+
if (menuBooleanKeys.has(key) && (value === 'true' || value === 'false'))
|
|
59
|
+
return { key: key, operator, value: value === 'true' };
|
|
60
|
+
if (menuStringKeys.has(key))
|
|
61
|
+
return { key: key, operator, value };
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
export function isValidPluginMenuCondition(condition) {
|
|
65
|
+
return condition.length > 0 && condition.length <= 240
|
|
66
|
+
&& condition.split('||').every(group => group.split('&&').every(atom => parseMenuAtom(atom) !== null));
|
|
67
|
+
}
|
|
68
|
+
export function matchesPluginMenuCondition(condition, context) {
|
|
69
|
+
if (condition === undefined)
|
|
70
|
+
return true;
|
|
71
|
+
if (!isValidPluginMenuCondition(condition))
|
|
72
|
+
return false;
|
|
73
|
+
return condition.split('||').some(group => group.split('&&').every(atom => {
|
|
74
|
+
const parsed = parseMenuAtom(atom);
|
|
75
|
+
const actual = context[parsed.key];
|
|
76
|
+
if (actual === undefined)
|
|
77
|
+
return false;
|
|
78
|
+
return parsed.operator === '==' ? actual === parsed.value : actual !== parsed.value;
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
/** Walk a UI document with shared depth/count limits, including nested forms. */
|
|
82
|
+
export function flattenPluginUiBlocks(blocks) {
|
|
83
|
+
const result = [];
|
|
84
|
+
function visit(items, depth) {
|
|
85
|
+
if (depth > 6)
|
|
86
|
+
throw new PluginError('QuotaExceeded', 'Plugin UI nesting exceeds 6 levels');
|
|
87
|
+
for (const block of items) {
|
|
88
|
+
result.push(block);
|
|
89
|
+
if (result.length > 200)
|
|
90
|
+
throw new PluginError('QuotaExceeded', 'Plugin UI exceeds 200 blocks');
|
|
91
|
+
if (block.type === 'layout' || block.type === 'section')
|
|
92
|
+
visit(block.blocks, depth + 1);
|
|
93
|
+
if (block.type === 'tabs')
|
|
94
|
+
for (const tab of block.tabs)
|
|
95
|
+
visit(tab.blocks, depth + 1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
visit(blocks, 0);
|
|
99
|
+
const ids = new Set();
|
|
100
|
+
for (const block of result) {
|
|
101
|
+
if ('id' in block && block.id !== undefined) {
|
|
102
|
+
const key = `${block.type}:${block.id}`;
|
|
103
|
+
if (ids.has(key))
|
|
104
|
+
throw new PluginError('InvalidPath', 'Duplicate UI block ID');
|
|
105
|
+
ids.add(key);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
/** Shared validation used by NoteGen and the SDK's in-memory host. */
|
|
111
|
+
export function parsePluginUiExtension(value, parseChildren) {
|
|
112
|
+
const parsed = parsePluginUiExtensionValue(value, parseChildren);
|
|
113
|
+
// Validation constructs optional fields explicitly. Omit absent fields from
|
|
114
|
+
// the normalized result so snapshots remain valid JSON on both host paths.
|
|
115
|
+
// Raw input and command arguments are validated before this normalization.
|
|
116
|
+
return parsed === undefined ? undefined : JSON.parse(JSON.stringify(parsed));
|
|
117
|
+
}
|
|
118
|
+
function parsePluginUiExtensionValue(value, parseChildren) {
|
|
119
|
+
const fail = () => { throw new PluginError('InvalidPath', 'Malformed extended plugin UI block'); };
|
|
120
|
+
const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : fail();
|
|
121
|
+
const text = (value, max = 500, min = 1) => typeof value === 'string' && value.length >= min && value.length <= max ? value : fail();
|
|
122
|
+
const boolean = (value) => value === undefined || typeof value === 'boolean' ? value : fail();
|
|
123
|
+
const choice = (value, options) => value === undefined ? undefined : options.includes(value) ? value : fail();
|
|
124
|
+
const keys = (value, allowed) => { if (Object.keys(value).some(key => !allowed.includes(key)))
|
|
125
|
+
fail(); };
|
|
126
|
+
const array = (value, max) => Array.isArray(value) && value.length <= max ? value : fail();
|
|
127
|
+
const unique = (items) => { if (new Set(items.map(item => item.id)).size !== items.length)
|
|
128
|
+
fail(); };
|
|
129
|
+
const optionalText = (value, max = 500) => value === undefined ? undefined : text(value, max);
|
|
130
|
+
const action = (value) => {
|
|
131
|
+
const v = record(value);
|
|
132
|
+
keys(v, ['id', 'label', 'command', 'argument', 'icon', 'iconOnly', 'confirmation', 'disabled', 'variant']);
|
|
133
|
+
// Arguments cross the existing JSON-only RPC boundary; reject non-JSON values here too.
|
|
134
|
+
const json = (value, depth = 0) => depth <= 12 && (value === null || typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number' && Number.isFinite(value) || Array.isArray(value) && value.every(item => json(item, depth + 1)) || typeof value === 'object' && value !== null && Object.values(value).every(item => json(item, depth + 1)));
|
|
135
|
+
if (v.argument !== undefined && !json(v.argument))
|
|
136
|
+
fail();
|
|
137
|
+
let confirmation;
|
|
138
|
+
if (v.confirmation !== undefined) {
|
|
139
|
+
const c = record(v.confirmation);
|
|
140
|
+
keys(c, ['title', 'description', 'confirmLabel', 'cancelLabel']);
|
|
141
|
+
confirmation = { title: text(c.title, 240), description: optionalText(c.description, 2000), confirmLabel: text(c.confirmLabel, 160), cancelLabel: text(c.cancelLabel, 160) };
|
|
142
|
+
}
|
|
143
|
+
if (v.iconOnly === true && !v.icon)
|
|
144
|
+
fail();
|
|
145
|
+
return { iconOnly: boolean(v.iconOnly), confirmation, id: text(v.id, 160), label: text(v.label, 160), command: text(v.command, 220), argument: v.argument, icon: optionalText(v.icon, 80), disabled: boolean(v.disabled), variant: choice(v.variant, ['default', 'secondary', 'destructive', 'ghost', 'outline']) };
|
|
146
|
+
};
|
|
147
|
+
const v = record(value);
|
|
148
|
+
switch (v.type) {
|
|
149
|
+
case 'layout':
|
|
150
|
+
keys(v, ['type', 'id', 'direction', 'gap', 'blocks']);
|
|
151
|
+
return { type: 'layout', id: text(v.id, 160), direction: choice(v.direction, ['row', 'column']), gap: choice(v.gap, ['small', 'medium', 'large']), blocks: parseChildren(v.blocks) };
|
|
152
|
+
case 'section':
|
|
153
|
+
keys(v, ['type', 'id', 'title', 'collapsible', 'defaultOpen', 'blocks']);
|
|
154
|
+
return { type: 'section', id: text(v.id, 160), title: text(v.title, 240), collapsible: boolean(v.collapsible), defaultOpen: boolean(v.defaultOpen), blocks: parseChildren(v.blocks) };
|
|
155
|
+
case 'tabs': {
|
|
156
|
+
keys(v, ['type', 'id', 'label', 'tabs']);
|
|
157
|
+
const tabs = array(v.tabs, 12).map(value => { const tab = record(value); keys(tab, ['id', 'label', 'blocks']); return { id: text(tab.id, 160), label: text(tab.label, 160), blocks: parseChildren(tab.blocks) }; });
|
|
158
|
+
if (!tabs.length)
|
|
159
|
+
fail();
|
|
160
|
+
unique(tabs);
|
|
161
|
+
return { type: 'tabs', id: text(v.id, 160), label: text(v.label, 160), tabs };
|
|
162
|
+
}
|
|
163
|
+
case 'toolbar': {
|
|
164
|
+
keys(v, ['type', 'id', 'label', 'actions']);
|
|
165
|
+
const actions = array(v.actions, 20).map(action);
|
|
166
|
+
unique(actions);
|
|
167
|
+
return { type: 'toolbar', id: text(v.id, 160), label: text(v.label, 160), actions };
|
|
168
|
+
}
|
|
169
|
+
case 'item-list': {
|
|
170
|
+
keys(v, ['type', 'id', 'generation', 'label', 'emptyText', 'items', 'openCommand', 'toggleCommand', 'reorderCommand', 'reorderLabel', 'actions']);
|
|
171
|
+
const items = array(v.items, 100).map(value => { const item = record(value); keys(item, ['id', 'label', 'description', 'icon', 'checked', 'disabled']); return { id: text(item.id, 1024), label: text(item.label), description: optionalText(item.description, 2000), icon: optionalText(item.icon, 80), checked: boolean(item.checked), disabled: boolean(item.disabled) }; });
|
|
172
|
+
unique(items);
|
|
173
|
+
const actions = v.actions === undefined ? undefined : array(v.actions, 20).map(action);
|
|
174
|
+
if (actions)
|
|
175
|
+
unique(actions);
|
|
176
|
+
if (v.reorderCommand !== undefined && v.reorderLabel === undefined)
|
|
177
|
+
fail();
|
|
178
|
+
return { type: 'item-list', id: text(v.id, 160), generation: text(v.generation, 160), label: text(v.label, 160), emptyText: text(v.emptyText, 500, 0), items, actions, openCommand: optionalText(v.openCommand, 220), toggleCommand: optionalText(v.toggleCommand, 220), reorderCommand: optionalText(v.reorderCommand, 220), reorderLabel: optionalText(v.reorderLabel, 160) };
|
|
179
|
+
}
|
|
180
|
+
case 'markdown':
|
|
181
|
+
keys(v, ['type', 'text']);
|
|
182
|
+
return { type: 'markdown', text: text(v.text, 20000, 0) };
|
|
183
|
+
case 'badge':
|
|
184
|
+
keys(v, ['type', 'text', 'tone']);
|
|
185
|
+
return { type: 'badge', text: text(v.text, 160), tone: choice(v.tone, ['default', 'secondary', 'outline', 'destructive']) };
|
|
186
|
+
case 'empty':
|
|
187
|
+
keys(v, ['type', 'title', 'description', 'icon']);
|
|
188
|
+
return { type: 'empty', title: text(v.title, 240), description: optionalText(v.description, 2000), icon: optionalText(v.icon, 80) };
|
|
189
|
+
case 'loading':
|
|
190
|
+
keys(v, ['type', 'label']);
|
|
191
|
+
return { type: 'loading', label: text(v.label, 160) };
|
|
192
|
+
default: return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@notegen/plugin-api",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Public TypeScript contract for NoteGen plugins.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"url": "https://github.com/codexu/note-gen-plugin-sdk/issues"
|
|
38
38
|
},
|
|
39
39
|
"notegen": {
|
|
40
|
-
"pluginApiVersion": "0.1.
|
|
40
|
+
"pluginApiVersion": "0.1.1"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"typescript": "^5.8.3"
|
|
@@ -186,6 +186,7 @@
|
|
|
186
186
|
"title": { "$ref": "#/$defs/localizedText" },
|
|
187
187
|
"description": { "$ref": "#/$defs/localizedText" },
|
|
188
188
|
"icon": { "type": "string", "pattern": "^[A-Za-z0-9-]+$", "maxLength": 80 },
|
|
189
|
+
"keywords": { "type": "array", "maxItems": 20, "items": { "type": "string", "minLength": 1, "maxLength": 80 } },
|
|
189
190
|
"suggestedShortcut": { "type": "string", "minLength": 1, "maxLength": 80 }
|
|
190
191
|
}
|
|
191
192
|
},
|
|
@@ -277,10 +278,13 @@
|
|
|
277
278
|
"additionalProperties": false,
|
|
278
279
|
"required": ["location", "command"],
|
|
279
280
|
"properties": {
|
|
280
|
-
"location": { "enum": ["editor/slash", "editor/context", "file/context", "mobile/writing/overflow"] },
|
|
281
|
+
"location": { "enum": ["editor/slash", "editor/context", "editor/selection", "editor/toolbar", "tab/context", "file/context", "mobile/writing/overflow"] },
|
|
281
282
|
"command": { "$ref": "#/$defs/namespacedId" },
|
|
282
283
|
"when": { "type": "string", "minLength": 1, "maxLength": 240 },
|
|
283
|
-
"group": { "type": "string", "minLength": 1, "maxLength": 80 }
|
|
284
|
+
"group": { "type": "string", "minLength": 1, "maxLength": 80 },
|
|
285
|
+
"enableWhen": { "type": "string", "minLength": 1, "maxLength": 240 },
|
|
286
|
+
"order": { "type": "integer", "minimum": -10000, "maximum": 10000 },
|
|
287
|
+
"icon": { "type": "string", "pattern": "^[A-Za-z0-9-]+$", "maxLength": 80 }
|
|
284
288
|
}
|
|
285
289
|
},
|
|
286
290
|
"view": {
|