@plannotator/ui 0.53.1 → 0.55.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/HANDOFF.md +274 -14
- package/README.md +114 -11
- package/components/AgentToolAnnouncementDialog.tsx +1 -1
- package/components/AgentsTab.tsx +1 -1
- package/components/AnnotationPanel.tsx +142 -24
- package/components/AnnotationSidebar.tsx +1 -1
- package/components/AnnotationToolbar.tsx +64 -9
- package/components/ApproveDropdown.tsx +7 -9
- package/components/AskSessionAnnouncementDialog.tsx +1 -1
- package/components/AttachmentsButton.tsx +1 -1
- package/components/BlockRenderer.tsx +22 -5
- package/components/CodeFilePopout.tsx +139 -4
- package/components/CodeSnippetPreview.tsx +174 -0
- package/components/CommentPopover.tsx +44 -8
- package/components/DecisionControl.tsx +31 -5
- package/components/DeferredMarkdownContentsRail.tsx +38 -0
- package/components/DiagramBlock.tsx +17 -12
- package/components/DiffFileTree.tsx +7 -6
- package/components/DocumentContents.tsx +74 -0
- package/components/EditorAnnotationCard.tsx +1 -1
- package/components/ExportModal.tsx +77 -7
- package/components/HostTitle.tsx +0 -4
- package/components/ImageAnnotator/Toolbar.tsx +25 -1
- package/components/ImageAnnotator/index.tsx +24 -4
- package/components/ImageLightbox.tsx +722 -126
- package/components/ImagePeek.tsx +38 -4
- package/components/ImageRegions.tsx +369 -0
- package/components/ImageThumbnail.tsx +1 -1
- package/components/ImageZoomStage.tsx +796 -0
- package/components/InboxAnnouncementDialog.tsx +202 -0
- package/components/InlineMarkdown.tsx +167 -174
- package/components/KeyboardShortcuts.tsx +23 -0
- package/components/LazyBlockBoundary.tsx +100 -0
- package/components/ListMarker.tsx +3 -3
- package/components/LookAndFeelAnnouncementDialog.tsx +12 -2
- package/components/MarkdownContentsRail.tsx +777 -0
- package/components/MenuVersionSection.tsx +31 -9
- package/components/PlanDecisionMenuExtras.tsx +129 -0
- package/components/PlanHeaderMenu.tsx +15 -1
- package/components/ProviderIcons.tsx +8 -0
- package/components/RefHoverCard.tsx +913 -0
- package/components/SendFailureToast.tsx +126 -0
- package/components/Settings.tsx +617 -126
- package/components/TableOfContents.tsx +20 -6
- package/components/TerminalToolsAnnouncementDialog.tsx +1 -1
- package/components/ThemeProvider.tsx +14 -1
- package/components/ThemeTab.tsx +16 -24
- package/components/Tooltip.tsx +49 -18
- package/components/Viewer.tsx +288 -43
- package/components/ai/DocumentAIChatPanel.tsx +29 -3
- package/components/ai/NewChatButton.tsx +74 -0
- package/components/blocks/CodeBlock.tsx +1 -1
- package/components/blocks/HtmlBlock.tsx +7 -1
- package/components/blocks/QuestionBlock.tsx +532 -62
- package/components/diagram/DiagramComposer.tsx +5 -1
- package/components/diagram/DiagramPending.tsx +12 -2
- package/components/diagram/DiagramViewer.tsx +19 -3
- package/components/html-embed/HtmlEmbedBlock.tsx +374 -0
- package/components/html-embed/SvgFenceBlock.tsx +46 -0
- package/components/html-embed/index.ts +3 -0
- package/components/html-embed/languages.ts +15 -0
- package/components/html-embed/loadGate.ts +107 -0
- package/components/html-viewer/HtmlViewer.tsx +82 -6
- package/components/html-viewer/bridge-script.asset.js +173 -1
- package/components/html-viewer/bridge-script.ts +173 -1
- package/components/html-viewer/useHtmlAnnotation.ts +125 -0
- package/components/imageZoom.ts +276 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +1 -1
- package/components/plan-diff/PlanDiffViewer.tsx +13 -2
- package/components/plan-diff/PlanRawDiffView.tsx +1 -1
- package/components/sidebar/FileBrowser.tsx +16 -2
- package/components/sidebar/SidebarTabs.tsx +3 -0
- package/config/index.ts +3 -0
- package/config/reviewView.ts +41 -1
- package/config/settings.ts +155 -4
- package/configure.ts +28 -0
- package/contents-rail.css +337 -0
- package/hooks/pfm/useCodeFilePopout.ts +7 -1
- package/hooks/useAIChat.ts +72 -25
- package/hooks/useAnnotationDraft.ts +71 -6
- package/hooks/useAnnotationHighlighter.ts +57 -3
- package/hooks/useCodeAnnotationDraft.ts +113 -17
- package/hooks/useCookieHostSetting.ts +96 -0
- package/hooks/useDocumentFont.ts +17 -0
- package/hooks/useDocumentTextSize.ts +20 -0
- package/hooks/useFileBrowser.ts +10 -1
- package/hooks/useHeadingScrollSpy.ts +77 -0
- package/hooks/useHoverPreview.ts +94 -0
- package/hooks/useLinkedDoc.ts +226 -13
- package/hooks/usePackageChangesSetting.ts +90 -0
- package/hooks/usePlanLookDialog.ts +67 -0
- package/hooks/usePrefersReducedMotion.ts +23 -0
- package/hooks/useSharing.ts +11 -7
- package/hooks/useSwipeStep.ts +597 -0
- package/hooks/useThemePaletteHalves.ts +37 -0
- package/hooks/useUpdateCheck.ts +22 -4
- package/hooks/useUpdateToast.ts +1 -1
- package/package.json +26 -23
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +1 -0
- package/shortcuts/code-review/explorer.shortcuts.ts +82 -0
- package/shortcuts/core.ts +63 -4
- package/shortcuts/index.ts +1 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +1 -1
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +1 -0
- package/shortcuts/runtime.ts +4 -3
- package/styles.css +1 -1
- package/theme.css +179 -1
- package/themes/plannotator-graphite.css +66 -0
- package/types.ts +26 -9
- package/utils/agentSwitch.ts +41 -20
- package/utils/agentToolSetting.ts +1 -1
- package/utils/aiChatFormat.ts +23 -7
- package/utils/annotationHelpers.ts +32 -4
- package/utils/annotationOwnsHighlight.ts +4 -2
- package/utils/asciidocParser.ts +570 -0
- package/utils/autoUpdateNotice.ts +24 -0
- package/utils/belowTitleSlot.ts +174 -0
- package/utils/blockParse.ts +45 -0
- package/utils/codeLineScroll.ts +100 -0
- package/utils/commentSubmitKey.ts +48 -0
- package/utils/cssFontFamily.ts +65 -0
- package/utils/decisionSpec.ts +190 -2
- package/utils/defaultNotesApp.ts +1 -1
- package/utils/diffFonts.ts +4 -1
- package/utils/documentFont.ts +68 -0
- package/utils/documentTextSize.ts +54 -0
- package/utils/fontDetect.ts +63 -0
- package/utils/headingScrollSpy.ts +92 -0
- package/utils/inboxAnnouncement.ts +78 -0
- package/utils/lookAndFeelAnnouncement.ts +82 -5
- package/utils/markdownExtensions.ts +2 -2
- package/utils/mermaid.ts +20 -3
- package/utils/mermaidTheme.ts +3 -51
- package/utils/notion.ts +58 -0
- package/utils/parser.ts +28 -1
- package/utils/platform.ts +46 -0
- package/utils/refCard.ts +82 -0
- package/utils/richClipboard.ts +380 -0
- package/utils/richClipboardLists.ts +57 -0
- package/utils/richCopyPolicy.ts +22 -0
- package/utils/scrollAnchor.ts +82 -0
- package/utils/sectionRefs.ts +123 -0
- package/utils/selectionBoundary.ts +40 -0
- package/utils/sendFailure.ts +85 -0
- package/utils/sharing.ts +5 -2
- package/utils/themeRegistry.ts +10 -0
- package/utils/themeTokens.ts +74 -0
- package/utils/updateNotice.ts +30 -8
- package/utils/vscodePanel.ts +9 -0
|
@@ -0,0 +1,570 @@
|
|
|
1
|
+
import type { AlertKind, Block } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A small AsciiDoc parser that splits a `.adoc` source into the same linear
|
|
5
|
+
* Block model as `parseMarkdownToBlocks`, so the Viewer, annotations, the
|
|
6
|
+
* export's line labels, drafts and sharing work unchanged. Block structure
|
|
7
|
+
* (sections, lists, delimited blocks, admonitions, tables, quotes) is parsed
|
|
8
|
+
* here; inline formatting is rewritten to markdown inline syntax because the
|
|
9
|
+
* Viewer's InlineMarkdown renderer speaks markdown.
|
|
10
|
+
*
|
|
11
|
+
* Every block keeps its real source span (`startLine` + `sourceLineCount`),
|
|
12
|
+
* so exported feedback names lines in the `.adoc` file itself.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately not supported (see docs/agent-guides/annotate.md): includes and
|
|
15
|
+
* conditionals are shown as plain text, not resolved; nested blocks inside
|
|
16
|
+
* admonitions, examples and table cells flatten to text; row spans; callout
|
|
17
|
+
* lists are ordinary paragraphs; the author and revision lines under the
|
|
18
|
+
* title are not shown. Anything unrecognized stays visible as text.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// Lines that start a block of their own and so end a description's text:
|
|
22
|
+
// list markers, a lone `+`, delimiters, attribute lists, comments, macros.
|
|
23
|
+
const DEFINITION_STOP_RE =
|
|
24
|
+
/^(?:(?:[*.]{1,5}|-|\d+\.)\s|\+$|(?:-{4,}|\.{4,}|={4,}|\*{4,}|_{4,}|\+{4,}|\|={3,}|--|```.*)$|\[.*\]$|\/\/|[a-z][\w-]*::\S*\[)/;
|
|
25
|
+
|
|
26
|
+
const ADMONITION_KINDS = new Set(['note', 'tip', 'important', 'warning', 'caution']);
|
|
27
|
+
|
|
28
|
+
/** Block metadata from attribute/anchor lines (`[source,js]`, `[NOTE]`,
|
|
29
|
+
* `[[id]]`, …) that attaches to the NEXT block. `startLine` is the first
|
|
30
|
+
* metadata line so the block's exported span covers it. */
|
|
31
|
+
interface PendingMeta {
|
|
32
|
+
startLine: number;
|
|
33
|
+
style?: string;
|
|
34
|
+
language?: string;
|
|
35
|
+
admonition?: AlertKind;
|
|
36
|
+
attribution?: string;
|
|
37
|
+
cols?: string;
|
|
38
|
+
options: Set<string>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const BUILTIN_ATTRIBUTES: Record<string, string> = {
|
|
42
|
+
nbsp: ' ',
|
|
43
|
+
empty: '',
|
|
44
|
+
sp: ' ',
|
|
45
|
+
plus: '+',
|
|
46
|
+
asterisk: '*',
|
|
47
|
+
tilde: '~',
|
|
48
|
+
backtick: '`',
|
|
49
|
+
startsb: '[',
|
|
50
|
+
endsb: ']',
|
|
51
|
+
vbar: '|',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Substitute `{name}` references. Unknown names stay literal. */
|
|
55
|
+
const substituteAttributes = (text: string, attrs: Map<string, string>): string =>
|
|
56
|
+
text.replace(/\{([a-zA-Z0-9_][a-zA-Z0-9_-]*)\}/g, (whole, name: string) =>
|
|
57
|
+
attrs.get(name) ?? BUILTIN_ATTRIBUTES[name] ?? whole,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// A local .adoc path; Antora resource ids (`module:page.adoc`, `example$x.adoc`) don't resolve here.
|
|
61
|
+
const isAsciidocDocTarget = (target: string): boolean => /^[^:$]+\.(?:adoc|asciidoc)(?:#.*)?$/i.test(target);
|
|
62
|
+
|
|
63
|
+
/** A cross reference: a link when it names another .adoc file (opens in the
|
|
64
|
+
* linked-doc overlay), else its text (in-page ids don't resolve here). */
|
|
65
|
+
const crossReference = (target: string, label: string): string => {
|
|
66
|
+
const text = label.trim();
|
|
67
|
+
if (isAsciidocDocTarget(target)) {
|
|
68
|
+
const path = target.replace(/#.*$/, '');
|
|
69
|
+
return `[${text || path}](${path})`;
|
|
70
|
+
}
|
|
71
|
+
return text || target.replace(/^#/, '');
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Rewrite AsciiDoc inline syntax to markdown for one non-code segment. */
|
|
75
|
+
const transpileSegment = (text: string, attrs: Map<string, string>): string => {
|
|
76
|
+
let out = substituteAttributes(text, attrs);
|
|
77
|
+
|
|
78
|
+
// Inline anchors (`[[id]]`, `anchor:id[]`) are invisible; left in, `[[id]]`
|
|
79
|
+
// would render as a markdown wiki-link.
|
|
80
|
+
out = out.replace(/\s*\[\[\[?[\w:.-]+(?:,[^\]]*)?\]\]\]?|anchor:[\w:.-]+\[[^\]]*\]/g, '');
|
|
81
|
+
out = out.replace(/\+\+\+(.*?)\+\+\+/g, '$1');
|
|
82
|
+
out = out.replace(/pass:[a-z,]*\[([^\]]*)\]/g, '$1');
|
|
83
|
+
|
|
84
|
+
out = out.replace(/image:([^:\s[][^[\s]*)\[([^\]]*)\]/g, (_, path: string, attrList: string) =>
|
|
85
|
+
`![${attrList.split(',')[0].trim()}](${path})`,
|
|
86
|
+
);
|
|
87
|
+
// Link text drops its `^` (open in a new window) marker.
|
|
88
|
+
const linkText = (label: string): string => label.split(',')[0].trim().replace(/\^$/, '');
|
|
89
|
+
out = out.replace(/link:([^\s[]+)\[([^\]]*)\]/g, (_, url: string, label: string) => `[${linkText(label) || url}](${url})`);
|
|
90
|
+
// URL followed by [text]. Bare URLs already autolink in InlineMarkdown.
|
|
91
|
+
out = out.replace(/(https?:\/\/[^\s[\]]+)\[([^\]]*)\]/g, (_, url: string, label: string) => {
|
|
92
|
+
const text = linkText(label);
|
|
93
|
+
return text ? `[${text}](${url})` : url;
|
|
94
|
+
});
|
|
95
|
+
out = out.replace(/xref:([^\s[]+)\[([^\]]*)\]/g, (_, target: string, label: string) => crossReference(target, label));
|
|
96
|
+
out = out.replace(/<<([^,<>\n]+)(?:,\s*([^<>\n]+))?>>/g, (_, target: string, label?: string) => crossReference(target.trim(), label ?? ''));
|
|
97
|
+
out = out.replace(/footnote:[\w-]*\[([^\]]*)\]/g, (_, note: string) => (note ? ` (${note})` : ''));
|
|
98
|
+
// A trailing ` +` is a hard line break; the line break itself is kept.
|
|
99
|
+
out = out.replace(/ \+$/gm, '');
|
|
100
|
+
|
|
101
|
+
// Constrained `*bold*` → `**bold**`; `**x**` is bold in both and is skipped.
|
|
102
|
+
out = out.replace(/(^|[^\w*])\*([^\s*](?:[^*]*[^\s*])?)\*(?![\w*])/g, '$1**$2**');
|
|
103
|
+
return out;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** Rewrite inline AsciiDoc to markdown, leaving backtick code spans alone
|
|
107
|
+
* (odd split segments) except for the `+literal+` markers inside them. */
|
|
108
|
+
export const transpileInline = (text: string, attrs: Map<string, string> = new Map()): string => {
|
|
109
|
+
if (!text) return text;
|
|
110
|
+
return text
|
|
111
|
+
.split('`')
|
|
112
|
+
.map((segment, idx) => (idx % 2 === 0 ? transpileSegment(segment, attrs) : segment.replace(/^\+(.+)\+$/, '$1')))
|
|
113
|
+
.join('`');
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
/** Parse a block attribute list body (`source,java`, `cols="1,2",options=header`,
|
|
117
|
+
* `%header`, `quote, Ada Lovelace`) into positional and named values. */
|
|
118
|
+
const parseAttributeList = (body: string): { positional: string[]; named: Map<string, string> } => {
|
|
119
|
+
const positional: string[] = [];
|
|
120
|
+
const named = new Map<string, string>();
|
|
121
|
+
const parts: string[] = body.match(/(?:[^,"']|"[^"]*"|'[^']*')+/g) ?? [];
|
|
122
|
+
// `[,ruby]` keeps its empty first position (the style).
|
|
123
|
+
if (body.trimStart().startsWith(',')) parts.unshift('');
|
|
124
|
+
for (const raw of parts) {
|
|
125
|
+
const part = raw.trim();
|
|
126
|
+
const eq = part.match(/^([\w-]+)\s*=\s*(.*)$/);
|
|
127
|
+
if (eq) named.set(eq[1].toLowerCase(), eq[2].replace(/^(["'])(.*)\1$/, '$2'));
|
|
128
|
+
else positional.push(part.replace(/^(["'])(.*)\1$/, '$2'));
|
|
129
|
+
}
|
|
130
|
+
return { positional, named };
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** Column count from a `cols` value: `1,2,3` → 3, `3*` → 3, `2*,1` → 3, `3` → 3. */
|
|
134
|
+
const columnCountFromSpec = (cols: string): number | null => {
|
|
135
|
+
if (/^\d+$/.test(cols.trim())) return Number(cols.trim()) || null;
|
|
136
|
+
const items = cols.split(/[,;]/).map((s) => s.trim()).filter(Boolean);
|
|
137
|
+
if (items.length === 0) return null;
|
|
138
|
+
return items.reduce((sum, item) => sum + Number(item.match(/^(\d+)\*/)?.[1] ?? 1), 0);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
interface TableCell {
|
|
142
|
+
text: string;
|
|
143
|
+
colspan: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// A cell spec sits right before a `|`: `2*` (duplicate), `2+` (colspan),
|
|
147
|
+
// `.2+` (rowspan), `^.^` (align), `a` (style). Mid-line it needs whitespace
|
|
148
|
+
// before it, so a one-letter cell (`|a|b`) stays a cell.
|
|
149
|
+
const CELL_SPEC = String.raw`(?:(\d+)\*)?(?:(\d+)(?:\.\d+)?\+|\.\d+\+)?(?:[<^>])?(?:\.[<^>])?[adehlmsv]?$`;
|
|
150
|
+
const LEADING_SPEC_RE = new RegExp(`^(\\s*)${CELL_SPEC}`);
|
|
151
|
+
const CELL_END_SPEC_RE = new RegExp(`(\\s)${CELL_SPEC}`);
|
|
152
|
+
// Highest `N*` repeat honored; a larger one is a typo, not a table.
|
|
153
|
+
const MAX_CELL_REPEAT = 100;
|
|
154
|
+
|
|
155
|
+
/** Split one table line into cells. Text before the first `|` continues the
|
|
156
|
+
* previous cell. Escaped `\|` stays inside a cell. */
|
|
157
|
+
const tableLineCells = (line: string): { leading: string; cells: TableCell[] } => {
|
|
158
|
+
const parts = line.split(/(?<!\\)\|/);
|
|
159
|
+
const cells: TableCell[] = [];
|
|
160
|
+
let leading = parts[0];
|
|
161
|
+
let pendingSpec = { dup: 1, colspan: 1 };
|
|
162
|
+
const takeSpec = (segment: string, re: RegExp): string => {
|
|
163
|
+
const spec = segment.match(re);
|
|
164
|
+
if (!spec || spec[0].trim() === '') {
|
|
165
|
+
pendingSpec = { dup: 1, colspan: 1 };
|
|
166
|
+
return segment;
|
|
167
|
+
}
|
|
168
|
+
pendingSpec = { dup: Math.min(Number(spec[2] ?? 1), MAX_CELL_REPEAT), colspan: Math.min(Number(spec[3] ?? 1), MAX_CELL_REPEAT) };
|
|
169
|
+
return segment.slice(0, segment.length - spec[0].length + spec[1].length);
|
|
170
|
+
};
|
|
171
|
+
if (parts.length > 1) leading = takeSpec(parts[0], LEADING_SPEC_RE);
|
|
172
|
+
for (let i = 1; i < parts.length; i++) {
|
|
173
|
+
const { dup, colspan } = pendingSpec;
|
|
174
|
+
const text = (i < parts.length - 1 ? takeSpec(parts[i], CELL_END_SPEC_RE) : parts[i]).trim().replace(/\\\|/g, '|');
|
|
175
|
+
for (let d = 0; d < dup; d++) cells.push({ text, colspan });
|
|
176
|
+
}
|
|
177
|
+
return { leading: leading.trim(), cells };
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/** Convert `|===` body lines to a pipe-markdown table. Cells flow into rows by
|
|
181
|
+
* column count (the `cols` attribute, else the first line's cell count). The
|
|
182
|
+
* first row is the header when `%header`/`options=header` says so, or when
|
|
183
|
+
* the first line is followed by a blank line (AsciiDoc's implicit header);
|
|
184
|
+
* otherwise the markdown header row is left empty. */
|
|
185
|
+
const asciidocTableToPipeMarkdown = (bodyLines: string[], meta: PendingMeta | null, attrs: Map<string, string>): string => {
|
|
186
|
+
const cells: TableCell[] = [];
|
|
187
|
+
let firstLineCells = 0;
|
|
188
|
+
let firstLineIndex = -1;
|
|
189
|
+
bodyLines.forEach((line, index) => {
|
|
190
|
+
if (line.trim() === '') return;
|
|
191
|
+
const { leading, cells: lineCells } = tableLineCells(line);
|
|
192
|
+
if (leading && cells.length > 0) cells[cells.length - 1].text += ` ${leading}`;
|
|
193
|
+
else if (leading) cells.push({ text: leading, colspan: 1 });
|
|
194
|
+
if (firstLineIndex === -1 && lineCells.length > 0) {
|
|
195
|
+
firstLineIndex = index;
|
|
196
|
+
firstLineCells = lineCells.reduce((n, c) => n + c.colspan, 0);
|
|
197
|
+
}
|
|
198
|
+
for (const cell of lineCells) cells.push(cell);
|
|
199
|
+
});
|
|
200
|
+
if (cells.length === 0) return '';
|
|
201
|
+
|
|
202
|
+
const columnCount = Math.max(1, (meta?.cols ? columnCountFromSpec(meta.cols) : null) ?? firstLineCells);
|
|
203
|
+
const rows: string[][] = [];
|
|
204
|
+
let row: string[] = [];
|
|
205
|
+
for (const cell of cells) {
|
|
206
|
+
row.push(cell.text);
|
|
207
|
+
for (let s = 1; s < cell.colspan; s++) row.push('');
|
|
208
|
+
if (row.length >= columnCount) {
|
|
209
|
+
rows.push(row);
|
|
210
|
+
row = [];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (row.length > 0) rows.push(row);
|
|
214
|
+
|
|
215
|
+
const options = meta?.options ?? new Set<string>();
|
|
216
|
+
const implicitHeader = firstLineIndex !== -1 && bodyLines[firstLineIndex + 1]?.trim() === '' && firstLineCells === columnCount;
|
|
217
|
+
const hasHeader = !options.has('noheader') && (options.has('header') || implicitHeader);
|
|
218
|
+
|
|
219
|
+
const format = (cellsInRow: string[]): string => {
|
|
220
|
+
const padded = [...cellsInRow];
|
|
221
|
+
while (padded.length < columnCount) padded.push('');
|
|
222
|
+
return `| ${padded.map((c) => transpileInline(c, attrs).replace(/\|/g, '\\|')).join(' | ')} |`;
|
|
223
|
+
};
|
|
224
|
+
const header = hasHeader ? rows.shift()! : [];
|
|
225
|
+
return [format(header), `|${' --- |'.repeat(columnCount)}`, ...rows.map(format)].join('\n');
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export const parseAsciidocToBlocks = (source: string): Block[] => {
|
|
229
|
+
const lines = source.replace(/\r\n?/g, '\n').split('\n');
|
|
230
|
+
const blocks: Block[] = [];
|
|
231
|
+
const attrs = new Map<string, string>();
|
|
232
|
+
let currentId = 0;
|
|
233
|
+
|
|
234
|
+
let buffer: string[] = [];
|
|
235
|
+
let bufferStartLine = 1;
|
|
236
|
+
let bufferEndLine = 1;
|
|
237
|
+
let pendingMeta: PendingMeta | null = null;
|
|
238
|
+
// Lines directly under the document title are its author and revision.
|
|
239
|
+
let headerLinesLeft = 0;
|
|
240
|
+
// A lone `+` attaches the next paragraph to the previous list item.
|
|
241
|
+
let listContinuation = false;
|
|
242
|
+
// Plain lines right after a list item (no blank line) continue its text.
|
|
243
|
+
let listItemOpen = false;
|
|
244
|
+
// `.` items seen per nesting level, so numbering continues past a block
|
|
245
|
+
// attached with `+`. Reset by any block that is not attached to the list.
|
|
246
|
+
let orderedCount: number[] = [];
|
|
247
|
+
|
|
248
|
+
const meta = (line: number): PendingMeta => (pendingMeta ??= { startLine: line, options: new Set() });
|
|
249
|
+
const takeMeta = (): PendingMeta | null => {
|
|
250
|
+
const m = pendingMeta;
|
|
251
|
+
pendingMeta = null;
|
|
252
|
+
return m;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const push = (block: Omit<Block, 'id' | 'order'>) => {
|
|
256
|
+
if (block.type !== 'list-item' && !listContinuation) orderedCount = [];
|
|
257
|
+
listContinuation = false;
|
|
258
|
+
listItemOpen = false;
|
|
259
|
+
blocks.push({ ...block, id: `block-${currentId++}`, order: currentId });
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/** Push a block that spans `startLine..endLine` (1-based, inclusive). */
|
|
263
|
+
const pushSpan = (block: Omit<Block, 'id' | 'order' | 'startLine' | 'sourceLineCount'>, startLine: number, endLine: number) =>
|
|
264
|
+
push({ ...block, startLine, sourceLineCount: endLine - startLine + 1 });
|
|
265
|
+
|
|
266
|
+
const flush = () => {
|
|
267
|
+
if (buffer.length === 0) return;
|
|
268
|
+
const m = takeMeta();
|
|
269
|
+
const startLine = m?.startLine ?? bufferStartLine;
|
|
270
|
+
const endLine = bufferEndLine;
|
|
271
|
+
const literal = /^\s/.test(buffer[0]);
|
|
272
|
+
const raw = buffer.join('\n');
|
|
273
|
+
buffer = [];
|
|
274
|
+
|
|
275
|
+
if (m?.style === 'source' || m?.style === 'listing' || m?.style === 'literal' || literal) {
|
|
276
|
+
const indent = Math.min(...raw.split('\n').filter((l) => l.trim()).map((l) => l.match(/^\s*/)![0].length));
|
|
277
|
+
const content = raw.split('\n').map((l) => l.slice(indent)).join('\n');
|
|
278
|
+
pushSpan({ type: 'code', content, language: m?.language }, startLine, endLine);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const content = transpileInline(raw.trim(), attrs);
|
|
283
|
+
const last = blocks[blocks.length - 1];
|
|
284
|
+
if (listContinuation && last?.type === 'list-item') {
|
|
285
|
+
last.content += `\n\n${content}`;
|
|
286
|
+
last.sourceLineCount = endLine - last.startLine + 1;
|
|
287
|
+
listContinuation = false;
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (m?.admonition) {
|
|
291
|
+
pushSpan({ type: 'blockquote', content, alertKind: m.admonition }, startLine, endLine);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (m?.style === 'quote' || m?.style === 'verse') {
|
|
295
|
+
pushSpan({ type: 'blockquote', content: m.attribution ? `${content}\n— ${m.attribution}` : content }, startLine, endLine);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
pushSpan({ type: 'paragraph', content }, startLine, endLine);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
/** Body lines after `startIdx` up to the line equal to `delimiter`;
|
|
302
|
+
* returns [body, closing index] (the last line when unclosed). */
|
|
303
|
+
const collectDelimited = (startIdx: number, delimiter: string): [string[], number] => {
|
|
304
|
+
const body: string[] = [];
|
|
305
|
+
for (let j = startIdx + 1; j < lines.length; j++) {
|
|
306
|
+
if (lines[j].trim() === delimiter) return [body, j];
|
|
307
|
+
body.push(lines[j]);
|
|
308
|
+
}
|
|
309
|
+
return [body, lines.length - 1];
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
for (let i = 0; i < lines.length; i++) {
|
|
313
|
+
const line = lines[i];
|
|
314
|
+
const trimmed = line.trim();
|
|
315
|
+
const lineNum = i + 1;
|
|
316
|
+
|
|
317
|
+
// Author and revision lines sit directly under the title; anything else
|
|
318
|
+
// (an attribute entry, a directive, a blank line) ends the lookup.
|
|
319
|
+
if (headerLinesLeft > 0) {
|
|
320
|
+
if (/^[^:/=[.*|<'+-]/.test(trimmed) && !/^[a-z][\w-]*::/.test(trimmed)) {
|
|
321
|
+
headerLinesLeft--;
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
headerLinesLeft = 0;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Block comment //// … ////, then line comments.
|
|
328
|
+
if (/^\/{4,}$/.test(trimmed)) {
|
|
329
|
+
flush();
|
|
330
|
+
i = collectDelimited(i, trimmed)[1];
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
// Only a `//` at column 0 is a comment; indented, it is literal text.
|
|
334
|
+
if (line.startsWith('//')) continue;
|
|
335
|
+
|
|
336
|
+
// Attribute entry `:name: value` / `:!name:` — recorded, not rendered.
|
|
337
|
+
const attrEntry = trimmed.match(/^:(!?)([a-zA-Z0-9_][a-zA-Z0-9_-]*)(!?):(?:\s+(.*))?$/);
|
|
338
|
+
if (attrEntry && buffer.length === 0) {
|
|
339
|
+
const [, unsetBefore, name, unsetAfter, value] = attrEntry;
|
|
340
|
+
if (unsetBefore || unsetAfter) attrs.delete(name);
|
|
341
|
+
else attrs.set(name, substituteAttributes((value ?? '').trim(), attrs));
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Section title: `=` (document title) … `======`.
|
|
346
|
+
const heading = line.match(/^(={1,6})\s+(\S.*)$/);
|
|
347
|
+
if (heading) {
|
|
348
|
+
flush();
|
|
349
|
+
const m = takeMeta();
|
|
350
|
+
const level = heading[1].length;
|
|
351
|
+
pushSpan({ type: 'heading', content: transpileInline(heading[2].trim(), attrs), level }, m?.startLine ?? lineNum, lineNum);
|
|
352
|
+
if (level === 1 && blocks.length === 1) headerLinesLeft = 2;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (/^'{3,}$/.test(trimmed) || trimmed === '<<<') {
|
|
357
|
+
flush();
|
|
358
|
+
takeMeta();
|
|
359
|
+
push({ type: 'hr', content: '', startLine: lineNum });
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Listing ----, literal ...., passthrough ++++ and markdown ``` fences → code.
|
|
364
|
+
const fence = trimmed.match(/^```\s*([\w+-]*)/);
|
|
365
|
+
if (/^(-{4,}|\.{4,}|\+{4,})$/.test(trimmed) || fence) {
|
|
366
|
+
flush();
|
|
367
|
+
const m = takeMeta();
|
|
368
|
+
const [body, closeIdx] = collectDelimited(i, fence ? '```' : trimmed);
|
|
369
|
+
const language = (fence?.[1] || undefined) ?? m?.language ?? (m?.style === 'source' ? attrs.get('source-language') : undefined);
|
|
370
|
+
pushSpan({ type: 'code', content: body.join('\n'), language }, m?.startLine ?? lineNum, closeIdx + 1);
|
|
371
|
+
i = closeIdx;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Example ====: an admonition body when tagged [NOTE] etc., otherwise a
|
|
376
|
+
// transparent grouping (delimiters vanish, inner lines parse normally).
|
|
377
|
+
if (/^={4,}$/.test(trimmed)) {
|
|
378
|
+
flush();
|
|
379
|
+
if (pendingMeta?.admonition) {
|
|
380
|
+
const m = takeMeta()!;
|
|
381
|
+
const [body, closeIdx] = collectDelimited(i, trimmed);
|
|
382
|
+
pushSpan({ type: 'blockquote', content: transpileInline(body.join('\n').trim(), attrs), alertKind: m.admonition }, m.startLine, closeIdx + 1);
|
|
383
|
+
i = closeIdx;
|
|
384
|
+
}
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Sidebar **** and open -- blocks: transparent grouping.
|
|
389
|
+
if (/^\*{4,}$/.test(trimmed) || trimmed === '--') {
|
|
390
|
+
flush();
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (/^_{4,}$/.test(trimmed)) {
|
|
395
|
+
flush();
|
|
396
|
+
const m = takeMeta();
|
|
397
|
+
const [body, closeIdx] = collectDelimited(i, trimmed);
|
|
398
|
+
let content = transpileInline(body.join('\n').trim(), attrs);
|
|
399
|
+
if (m?.attribution) content += `\n— ${m.attribution}`;
|
|
400
|
+
pushSpan({ type: 'blockquote', content }, m?.startLine ?? lineNum, closeIdx + 1);
|
|
401
|
+
i = closeIdx;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (/^\|={3,}$/.test(trimmed)) {
|
|
406
|
+
flush();
|
|
407
|
+
const m = takeMeta();
|
|
408
|
+
const [body, closeIdx] = collectDelimited(i, trimmed);
|
|
409
|
+
const content = asciidocTableToPipeMarkdown(body, m, attrs);
|
|
410
|
+
if (content) pushSpan({ type: 'table', content }, m?.startLine ?? lineNum, closeIdx + 1);
|
|
411
|
+
i = closeIdx;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Anchor `[[id]]` / block attributes `[source,js]`, `[NOTE]`, `[%header]`…
|
|
416
|
+
if (/^\[\[[^\]]+\]\]$/.test(trimmed) && buffer.length === 0) {
|
|
417
|
+
meta(lineNum);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
const attrLine = trimmed.match(/^\[([^[\]].*)\]$/);
|
|
421
|
+
if (attrLine && buffer.length === 0) {
|
|
422
|
+
const { positional, named } = parseAttributeList(attrLine[1]);
|
|
423
|
+
const m = meta(lineNum);
|
|
424
|
+
const [first = '', ...rest] = positional;
|
|
425
|
+
const style = first.split(/[#.%]/)[0];
|
|
426
|
+
for (const option of first.match(/%[\w-]+/g) ?? []) m.options.add(option.slice(1).toLowerCase());
|
|
427
|
+
for (const option of (named.get('options') ?? named.get('opts') ?? '').split(',')) {
|
|
428
|
+
if (option.trim()) m.options.add(option.trim().toLowerCase());
|
|
429
|
+
}
|
|
430
|
+
if (named.has('cols')) m.cols = named.get('cols');
|
|
431
|
+
if (ADMONITION_KINDS.has(style.toLowerCase()) && style === style.toUpperCase()) {
|
|
432
|
+
m.admonition = style.toLowerCase() as AlertKind;
|
|
433
|
+
} else if (style || (first === '' && rest.length > 0)) {
|
|
434
|
+
// `[,ruby]` is the shorthand for `[source,ruby]`.
|
|
435
|
+
m.style = style ? style.toLowerCase() : 'source';
|
|
436
|
+
if (m.style === 'source') m.language = rest[0] || named.get('language') || undefined;
|
|
437
|
+
if (m.style === 'quote' || m.style === 'verse') m.attribution = rest.filter(Boolean).join(', ') || undefined;
|
|
438
|
+
}
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Admonition paragraph: `NOTE: text` up to the next blank line.
|
|
443
|
+
const admonition = trimmed.match(/^(NOTE|TIP|IMPORTANT|WARNING|CAUTION):\s+(.*)$/);
|
|
444
|
+
if (admonition && buffer.length === 0) {
|
|
445
|
+
const m = takeMeta();
|
|
446
|
+
const startLine = m?.startLine ?? lineNum;
|
|
447
|
+
const body = [admonition[2]];
|
|
448
|
+
while (i + 1 < lines.length && lines[i + 1].trim() !== '') body.push(lines[++i].trim());
|
|
449
|
+
pushSpan({ type: 'blockquote', content: transpileInline(body.join('\n'), attrs), alertKind: admonition[1].toLowerCase() as AlertKind }, startLine, i + 1);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const blockImage = trimmed.match(/^image::([^[\s]+)\[([^\]]*)\]$/);
|
|
454
|
+
if (blockImage) {
|
|
455
|
+
flush();
|
|
456
|
+
const m = takeMeta();
|
|
457
|
+
pushSpan({ type: 'paragraph', content: `![${blockImage[2].split(',')[0].trim()}](${blockImage[1]})` }, m?.startLine ?? lineNum, lineNum);
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Other block macros and preprocessor directives (`include::x.adoc[]`,
|
|
462
|
+
// `ifdef::attr[]`, `toc::[]`) are shown as written, never resolved.
|
|
463
|
+
if (/^[a-z][\w-]*::\S*\[.*\]$/.test(trimmed)) {
|
|
464
|
+
flush();
|
|
465
|
+
const m = takeMeta();
|
|
466
|
+
pushSpan({ type: 'paragraph', content: trimmed.includes('`') ? trimmed : `\`${trimmed}\`` }, m?.startLine ?? lineNum, lineNum);
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Lists: `*`…`*****` / `-` (unordered), `.`…`.....` / `1.` (ordered).
|
|
471
|
+
const unordered = line.match(/^\s*(\*{1,5}|-)\s+(.*\S)\s*$/);
|
|
472
|
+
const ordered = line.match(/^\s*(\.{1,5}|\d+\.)\s+(.*\S)\s*$/);
|
|
473
|
+
const item = unordered ?? ordered;
|
|
474
|
+
if (item) {
|
|
475
|
+
flush();
|
|
476
|
+
takeMeta();
|
|
477
|
+
const marker = item[1];
|
|
478
|
+
let content = item[2];
|
|
479
|
+
let checked: boolean | undefined;
|
|
480
|
+
const checkbox = unordered ? content.match(/^\[([ xX*])\]\s+/) : null;
|
|
481
|
+
if (checkbox) {
|
|
482
|
+
checked = checkbox[1] !== ' ';
|
|
483
|
+
content = content.slice(checkbox[0].length);
|
|
484
|
+
}
|
|
485
|
+
const numbered = /^\d/.test(marker);
|
|
486
|
+
const level = marker === '-' || numbered ? 0 : marker.length - 1;
|
|
487
|
+
orderedCount.length = level + 1;
|
|
488
|
+
let orderedStart: number | undefined;
|
|
489
|
+
if (ordered) {
|
|
490
|
+
orderedCount[level] = numbered ? Number.parseInt(marker, 10) : (orderedCount[level] ?? 0) + 1;
|
|
491
|
+
// A new list group (after an attached block) carries on the count.
|
|
492
|
+
if (numbered || (blocks[blocks.length - 1]?.type !== 'list-item' && orderedCount[level] > 1)) orderedStart = orderedCount[level];
|
|
493
|
+
} else {
|
|
494
|
+
orderedCount[level] = 0;
|
|
495
|
+
}
|
|
496
|
+
push({
|
|
497
|
+
type: 'list-item',
|
|
498
|
+
content: transpileInline(content, attrs),
|
|
499
|
+
level,
|
|
500
|
+
ordered: ordered ? true : undefined,
|
|
501
|
+
orderedStart,
|
|
502
|
+
checked,
|
|
503
|
+
startLine: lineNum,
|
|
504
|
+
});
|
|
505
|
+
listItemOpen = true;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (trimmed === '+' && buffer.length === 0 && blocks[blocks.length - 1]?.type === 'list-item') {
|
|
510
|
+
listContinuation = true;
|
|
511
|
+
listItemOpen = false;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Description list: `term:: definition` (the definition may follow on the
|
|
516
|
+
// next lines). Deeper `:::` / `;;` markers read the same.
|
|
517
|
+
const term = trimmed.match(/^(\S.*?)(?::{2,4}|;;)(?:\s+(.*))?$/);
|
|
518
|
+
if (term && buffer.length === 0) {
|
|
519
|
+
takeMeta();
|
|
520
|
+
const startLine = lineNum;
|
|
521
|
+
const definition = term[2] ? [term[2]] : [];
|
|
522
|
+
// The definition runs to a blank line, the next term, or a line that
|
|
523
|
+
// starts its own block (list marker, `+`, delimiter, attribute list).
|
|
524
|
+
while (
|
|
525
|
+
i + 1 < lines.length &&
|
|
526
|
+
lines[i + 1].trim() !== '' &&
|
|
527
|
+
!/^(\S.*?)(?::{2,4}|;;)(?:\s|$)/.test(lines[i + 1].trim()) &&
|
|
528
|
+
!DEFINITION_STOP_RE.test(lines[i + 1].trim())
|
|
529
|
+
) {
|
|
530
|
+
definition.push(lines[++i].trim());
|
|
531
|
+
}
|
|
532
|
+
const text = transpileInline(definition.join('\n'), attrs);
|
|
533
|
+
pushSpan({ type: 'list-item', content: `**${transpileInline(term[1], attrs)}**${text ? ` — ${text}` : ''}`, level: 0 }, startLine, i + 1);
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Block title `.Title` → bold caption paragraph.
|
|
538
|
+
const title = trimmed.match(/^\.([^\s.].*)$/);
|
|
539
|
+
if (title && buffer.length === 0) {
|
|
540
|
+
// Attributes above the title still belong to the block below it, and a
|
|
541
|
+
// `+` above it still attaches that block to the list.
|
|
542
|
+
if (pendingMeta) pendingMeta.startLine = lineNum + 1;
|
|
543
|
+
const attached = listContinuation;
|
|
544
|
+
push({ type: 'paragraph', content: `**${transpileInline(title[1].trim(), attrs)}**`, startLine: lineNum });
|
|
545
|
+
listContinuation = attached;
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (trimmed === '') {
|
|
550
|
+
flush();
|
|
551
|
+
listItemOpen = false;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// A plain line right under a list item continues that item.
|
|
556
|
+
const last = blocks[blocks.length - 1];
|
|
557
|
+
if (listItemOpen && buffer.length === 0 && last?.type === 'list-item') {
|
|
558
|
+
last.content += `\n${transpileInline(trimmed, attrs)}`;
|
|
559
|
+
last.sourceLineCount = lineNum - last.startLine + 1;
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (buffer.length === 0) bufferStartLine = lineNum;
|
|
564
|
+
bufferEndLine = lineNum;
|
|
565
|
+
buffer.push(line);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
flush();
|
|
569
|
+
return blocks;
|
|
570
|
+
};
|
|
@@ -17,6 +17,11 @@ export interface AutoUpdateNotice {
|
|
|
17
17
|
version: string;
|
|
18
18
|
releaseUrl: string;
|
|
19
19
|
logPath?: string;
|
|
20
|
+
/**
|
|
21
|
+
* `mismatch`: the download did not match its checksum; `download`: it could not be downloaded;
|
|
22
|
+
* `prepare`: it was downloaded but the temp folder could not hold it. The old version is kept in each case.
|
|
23
|
+
*/
|
|
24
|
+
reason?: 'mismatch' | 'download' | 'prepare';
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
const SEEN_KEY = 'plannotator-auto-update-notice-seen';
|
|
@@ -34,6 +39,7 @@ export function parseAutoUpdateNotice(value: unknown): AutoUpdateNotice | undefi
|
|
|
34
39
|
version: v.version,
|
|
35
40
|
releaseUrl: v.releaseUrl,
|
|
36
41
|
...(typeof v.logPath === 'string' && { logPath: v.logPath }),
|
|
42
|
+
...((v.reason === 'mismatch' || v.reason === 'download' || v.reason === 'prepare') && { reason: v.reason }),
|
|
37
43
|
};
|
|
38
44
|
}
|
|
39
45
|
|
|
@@ -52,6 +58,24 @@ export function describeAutoUpdateNotice(notice: AutoUpdateNotice): { title: str
|
|
|
52
58
|
if (notice.kind === 'updated') {
|
|
53
59
|
return { title: `Updated to v${notice.version}`, description: 'Plannotator updated itself in the background.' };
|
|
54
60
|
}
|
|
61
|
+
if (notice.reason === 'mismatch') {
|
|
62
|
+
return {
|
|
63
|
+
title: `Update to v${notice.version} stopped`,
|
|
64
|
+
description: "The download didn't match the checksum Plannotator fetched for it. Nothing was changed; it tries again later.",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (notice.reason === 'prepare') {
|
|
68
|
+
return {
|
|
69
|
+
title: "Couldn't prepare the update",
|
|
70
|
+
description: 'The temp folder couldn’t hold it. Nothing was changed. Run plannotator update to see why and what to do.',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (notice.reason === 'download') {
|
|
74
|
+
return {
|
|
75
|
+
title: "Couldn't download the update",
|
|
76
|
+
description: 'Nothing was changed. Try again later: plannotator update',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
55
79
|
return {
|
|
56
80
|
title: 'Auto-update failed',
|
|
57
81
|
description: notice.logPath ? `See ${notice.logPath}` : `Could not install v${notice.version}.`,
|