canopycms 0.1.0-int.115 → 0.1.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/CHANGELOG.md +267 -231
- package/README.md +0 -1
- package/dist/ai/references.js +3 -1
- package/dist/api/assets.js +2 -2
- package/dist/api/branch-merge.d.ts +1 -1
- package/dist/api/branch.js +1 -1
- package/dist/api/content.js +1 -1
- package/dist/api/entries-constants.d.ts +1 -1
- package/dist/api/entries-constants.js +1 -1
- package/dist/api/entries.d.ts +2 -2
- package/dist/api/permissions.d.ts +2 -2
- package/dist/api/types.d.ts +3 -2
- package/dist/cli/cli.js +16 -14
- package/dist/cli/generate-ai-content.js +7 -5
- package/dist/cli/init.js +4 -4
- package/dist/client.d.ts +0 -1
- package/dist/client.js +0 -1
- package/dist/config/schemas/config.d.ts +0 -5
- package/dist/config/schemas/config.js +0 -1
- package/dist/config/types.d.ts +0 -1
- package/dist/content-index-generation.d.ts +1 -1
- package/dist/content-index-generation.js +1 -1
- package/dist/content-listing.js +1 -1
- package/dist/content-reader.js +1 -1
- package/dist/content-store.d.ts +1 -1
- package/dist/content-store.js +1 -1
- package/dist/dev-content-watcher.d.ts +1 -1
- package/dist/dev-content-watcher.js +1 -1
- package/dist/editor/BranchManager.d.ts +2 -0
- package/dist/editor/BranchManager.js +5 -4
- package/dist/editor/CanopyEditor.d.ts +1 -1
- package/dist/editor/CanopyEditor.js +1 -2
- package/dist/editor/CommentsPanel.js +3 -6
- package/dist/editor/Editor.d.ts +0 -2
- package/dist/editor/Editor.js +329 -181
- package/dist/editor/EditorAuthGate.js +8 -3
- package/dist/editor/EntryNavigator.d.ts +10 -0
- package/dist/editor/EntryNavigator.js +51 -2
- package/dist/editor/FormHeader.js +1 -1
- package/dist/editor/PreviewFrame.d.ts +5 -1
- package/dist/editor/PreviewFrame.js +3 -2
- package/dist/editor/PreviewPane.d.ts +47 -0
- package/dist/editor/PreviewPane.js +147 -0
- package/dist/editor/PreviewToolbarTools.d.ts +22 -0
- package/dist/editor/PreviewToolbarTools.js +73 -0
- package/dist/editor/comments/form-comments.js +2 -1
- package/dist/editor/components/BranchLockBanner.d.ts +22 -0
- package/dist/editor/components/BranchLockBanner.js +39 -0
- package/dist/editor/components/BranchPill.d.ts +32 -0
- package/dist/editor/components/BranchPill.js +33 -0
- package/dist/editor/components/ConfirmDeleteModal.d.ts +3 -1
- package/dist/editor/components/ConfirmDeleteModal.js +3 -2
- package/dist/editor/components/EditorHeader.d.ts +50 -108
- package/dist/editor/components/EditorHeader.js +117 -138
- package/dist/editor/components/EditorRail.d.ts +49 -0
- package/dist/editor/components/EditorRail.js +65 -0
- package/dist/editor/components/EntryBreadcrumb.d.ts +33 -0
- package/dist/editor/components/EntryBreadcrumb.js +56 -0
- package/dist/editor/components/QuickOpenPalette.d.ts +1 -1
- package/dist/editor/components/QuickOpenPalette.js +3 -2
- package/dist/editor/components/RenameEntryModal.d.ts +3 -1
- package/dist/editor/components/RenameEntryModal.js +3 -2
- package/dist/editor/components/SaveAllResult.d.ts +25 -0
- package/dist/editor/components/SaveAllResult.js +86 -0
- package/dist/editor/components/SaveSplitButton.d.ts +30 -0
- package/dist/editor/components/SaveSplitButton.js +108 -0
- package/dist/editor/components/index.d.ts +2 -2
- package/dist/editor/components/index.js +2 -2
- package/dist/editor/context/EditorStateContext.d.ts +4 -4
- package/dist/editor/context/EditorStateContext.js +4 -4
- package/dist/editor/copy.d.ts +3 -0
- package/dist/editor/copy.js +3 -0
- package/dist/editor/editor-utils.d.ts +11 -6
- package/dist/editor/editor-utils.js +8 -17
- package/dist/editor/fields/MarkdownField.js +3 -0
- package/dist/editor/hooks/save-all.d.ts +68 -0
- package/dist/editor/hooks/save-all.js +61 -0
- package/dist/editor/hooks/useCommentSystem.d.ts +2 -0
- package/dist/editor/hooks/useCommentSystem.js +3 -0
- package/dist/editor/hooks/useDraftManager.d.ts +24 -1
- package/dist/editor/hooks/useDraftManager.js +297 -16
- package/dist/editor/hooks/useEditorLayout.d.ts +15 -11
- package/dist/editor/hooks/useEditorLayout.js +13 -25
- package/dist/editor/hooks/useEntryManager.d.ts +4 -0
- package/dist/editor/hooks/useEntryManager.js +30 -7
- package/dist/editor/media/MediaLibrary.d.ts +1 -1
- package/dist/editor/preview-path.d.ts +12 -0
- package/dist/editor/preview-path.js +45 -1
- package/dist/editor/preview-scale.d.ts +27 -0
- package/dist/editor/preview-scale.js +34 -0
- package/dist/editor/shell/ActivityRail.d.ts +37 -0
- package/dist/editor/shell/ActivityRail.js +91 -0
- package/dist/editor/shell/EditorShell.d.ts +17 -0
- package/dist/editor/shell/EditorShell.js +31 -0
- package/dist/editor/shell/PaneLayout.d.ts +28 -0
- package/dist/editor/shell/PaneLayout.js +116 -0
- package/dist/editor/shell/SidePanelHost.d.ts +35 -0
- package/dist/editor/shell/SidePanelHost.js +184 -0
- package/dist/editor/shell/TopBar.d.ts +12 -0
- package/dist/editor/shell/TopBar.js +14 -0
- package/dist/editor/shell/host-chrome.d.ts +16 -0
- package/dist/editor/shell/host-chrome.js +69 -0
- package/dist/editor/theme.js +18 -3
- package/dist/git-manager.d.ts +1 -1
- package/dist/git-manager.js +2 -2
- package/dist/operating-mode/types.d.ts +1 -1
- package/dist/resolve-canopy-user.d.ts +1 -1
- package/dist/resolve-canopy-user.js +1 -1
- package/dist/schema/resolver.d.ts +1 -1
- package/dist/schema/resolver.js +1 -1
- package/dist/schema/schema-store.d.ts +1 -1
- package/dist/schema/schema-store.js +2 -2
- package/dist/url-collision.js +3 -3
- package/dist/url-exclusivity-fixtures.d.ts +1 -1
- package/dist/url-exclusivity-fixtures.js +1 -1
- package/dist/utils/content-write-lock.d.ts +1 -1
- package/dist/utils/content-write-lock.js +1 -1
- package/dist/utils/debug.d.ts +1 -1
- package/dist/utils/debug.js +5 -3
- package/dist/utils/git.d.ts +4 -0
- package/dist/utils/git.js +27 -5
- package/dist/validation/block-structural-keys.d.ts +1 -1
- package/dist/validation/block-structural-keys.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker/cms-worker.d.ts +1 -1
- package/dist/worker/cms-worker.js +2 -2
- package/dist/worker/github-auth.d.ts +1 -1
- package/dist/worker/github-auth.js +1 -1
- package/dist/worker/github-gateway-wire.d.ts +2 -1
- package/dist/worker/github-gateway-wire.js +7 -3
- package/dist/worker/github-gateway.js +1 -1
- package/dist/worker/github-mirror.d.ts +2 -2
- package/dist/worker/github-mirror.js +2 -2
- package/dist/worker/history-rewrite.d.ts +1 -1
- package/dist/worker/history-rewrite.js +1 -1
- package/dist/worker/self-checks.d.ts +1 -1
- package/dist/worker/shared-repo-git.js +1 -1
- package/package.json +5 -4
- package/dist/editor/EditorPanes.d.ts +0 -20
- package/dist/editor/EditorPanes.js +0 -124
- package/dist/editor/components/EditorFooter.d.ts +0 -11
- package/dist/editor/components/EditorFooter.js +0 -9
- package/dist/editor/components/EditorSidebar.d.ts +0 -70
- package/dist/editor/components/EditorSidebar.js +0 -50
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useRef, useState, } from 'react';
|
|
4
|
+
import { CloseButton, Group, Text, Tooltip } from '@mantine/core';
|
|
5
|
+
import { useWindowEvent } from '@mantine/hooks';
|
|
6
|
+
import { focusIfIdle } from '../utils/focus-return.js';
|
|
7
|
+
const KEY_STEP_PX = 16;
|
|
8
|
+
const RESIZING_ATTR = 'data-canopy-panel-resizing';
|
|
9
|
+
const PANEL_STYLES = `
|
|
10
|
+
body[${RESIZING_ATTR}] { cursor: col-resize; user-select: none; }
|
|
11
|
+
body[${RESIZING_ATTR}] iframe { pointer-events: none; }
|
|
12
|
+
.canopy-side-panel-separator {
|
|
13
|
+
position: absolute;
|
|
14
|
+
inset-block: 0;
|
|
15
|
+
inset-inline-end: -3px;
|
|
16
|
+
width: 6px;
|
|
17
|
+
cursor: col-resize;
|
|
18
|
+
touch-action: none;
|
|
19
|
+
z-index: 1;
|
|
20
|
+
}
|
|
21
|
+
.canopy-side-panel-separator:hover,
|
|
22
|
+
.canopy-side-panel-separator[data-dragging] {
|
|
23
|
+
background: var(--canopy-focus-ring);
|
|
24
|
+
}
|
|
25
|
+
.canopy-side-panel-separator:focus-visible {
|
|
26
|
+
outline: 2px solid var(--canopy-focus-ring);
|
|
27
|
+
outline-offset: -2px;
|
|
28
|
+
}
|
|
29
|
+
`;
|
|
30
|
+
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
31
|
+
/**
|
|
32
|
+
* A titled side panel, resizable from its inline-end edge. Docked, it is a column of the shell;
|
|
33
|
+
* as an overlay it covers the form pane only, so the preview stays in view, and Escape closes it.
|
|
34
|
+
* A `focusRequest` moves focus to its `data-autofocus` element, else to the panel; closing with
|
|
35
|
+
* focus inside returns focus to `returnFocusTo`.
|
|
36
|
+
*/
|
|
37
|
+
export function SidePanelHost({ title, mode, opened, width, minWidth, maxWidth, onWidthChange, onClose, focusRequest = 0, returnFocusTo, actions, children, id, 'data-testid': testId, }) {
|
|
38
|
+
const fallbackId = useId();
|
|
39
|
+
const panelId = id ?? fallbackId;
|
|
40
|
+
const titleId = `${panelId}-title`;
|
|
41
|
+
const panelRef = useRef(null);
|
|
42
|
+
const focusInside = useRef(false);
|
|
43
|
+
const wasOpened = useRef(opened);
|
|
44
|
+
const servedFocusRequest = useRef(focusRequest);
|
|
45
|
+
const drag = useRef(null);
|
|
46
|
+
const [dragWidth, setDragWidth] = useState(null);
|
|
47
|
+
if (!opened && dragWidth !== null)
|
|
48
|
+
setDragWidth(null);
|
|
49
|
+
const shownWidth = clamp(dragWidth ?? width, minWidth, maxWidth);
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
const panel = panelRef.current;
|
|
52
|
+
const asked = opened && focusRequest !== servedFocusRequest.current;
|
|
53
|
+
servedFocusRequest.current = focusRequest;
|
|
54
|
+
if (!asked || !panel)
|
|
55
|
+
return;
|
|
56
|
+
const autofocus = () => panel.querySelector('[data-autofocus]');
|
|
57
|
+
const field = autofocus();
|
|
58
|
+
if (field) {
|
|
59
|
+
field.focus({ preventScroll: true });
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
// The field may render with content still loading: it takes focus when it arrives, unless
|
|
63
|
+
// focus has moved off the panel by then.
|
|
64
|
+
panel.focus({ preventScroll: true });
|
|
65
|
+
const observer = new MutationObserver(() => {
|
|
66
|
+
if (document.activeElement !== panel)
|
|
67
|
+
return observer.disconnect();
|
|
68
|
+
const arrived = autofocus();
|
|
69
|
+
if (!arrived)
|
|
70
|
+
return;
|
|
71
|
+
observer.disconnect();
|
|
72
|
+
arrived.focus({ preventScroll: true });
|
|
73
|
+
});
|
|
74
|
+
observer.observe(panel, { childList: true, subtree: true });
|
|
75
|
+
return () => observer.disconnect();
|
|
76
|
+
}, [opened, focusRequest]);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!opened && wasOpened.current) {
|
|
79
|
+
// Its separator is gone, and with it the pointer capture that would have ended a drag.
|
|
80
|
+
drag.current = null;
|
|
81
|
+
document.body.removeAttribute(RESIZING_ATTR);
|
|
82
|
+
if (focusInside.current) {
|
|
83
|
+
focusInside.current = false;
|
|
84
|
+
focusIfIdle(returnFocusTo?.() ?? null);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
wasOpened.current = opened;
|
|
88
|
+
}, [opened, returnFocusTo]);
|
|
89
|
+
// On the window, not the panel: a dialog portalled from the panel would bubble its Escape here
|
|
90
|
+
// through React, so only a key from inside the panel's own DOM closes it.
|
|
91
|
+
useWindowEvent('keydown', (event) => {
|
|
92
|
+
if (opened &&
|
|
93
|
+
mode === 'overlay' &&
|
|
94
|
+
event.key === 'Escape' &&
|
|
95
|
+
!event.defaultPrevented &&
|
|
96
|
+
event.target instanceof Node &&
|
|
97
|
+
panelRef.current?.contains(event.target)) {
|
|
98
|
+
event.preventDefault();
|
|
99
|
+
onClose();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
// A drag cut short by unmounting must not leave the page's iframes deaf to the pointer.
|
|
103
|
+
useEffect(() => () => document.body.removeAttribute(RESIZING_ATTR), []);
|
|
104
|
+
if (!opened)
|
|
105
|
+
return null;
|
|
106
|
+
const endDrag = () => {
|
|
107
|
+
const state = drag.current;
|
|
108
|
+
if (!state)
|
|
109
|
+
return;
|
|
110
|
+
drag.current = null;
|
|
111
|
+
document.body.removeAttribute(RESIZING_ATTR);
|
|
112
|
+
setDragWidth(null);
|
|
113
|
+
onWidthChange(state.width);
|
|
114
|
+
};
|
|
115
|
+
const separatorHandlers = {
|
|
116
|
+
onPointerDown: (event) => {
|
|
117
|
+
if (event.button !== 0)
|
|
118
|
+
return;
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
121
|
+
const rtl = getComputedStyle(event.currentTarget).direction === 'rtl';
|
|
122
|
+
drag.current = {
|
|
123
|
+
startX: event.clientX,
|
|
124
|
+
startWidth: shownWidth,
|
|
125
|
+
width: shownWidth,
|
|
126
|
+
sign: rtl ? -1 : 1,
|
|
127
|
+
};
|
|
128
|
+
document.body.setAttribute(RESIZING_ATTR, '');
|
|
129
|
+
setDragWidth(shownWidth);
|
|
130
|
+
},
|
|
131
|
+
onPointerMove: (event) => {
|
|
132
|
+
const state = drag.current;
|
|
133
|
+
if (!state)
|
|
134
|
+
return;
|
|
135
|
+
state.width = clamp(state.startWidth + state.sign * (event.clientX - state.startX), minWidth, maxWidth);
|
|
136
|
+
setDragWidth(state.width);
|
|
137
|
+
},
|
|
138
|
+
onPointerUp: endDrag,
|
|
139
|
+
onPointerCancel: endDrag,
|
|
140
|
+
onLostPointerCapture: endDrag,
|
|
141
|
+
onKeyDown: (event) => {
|
|
142
|
+
const rtl = getComputedStyle(event.currentTarget).direction === 'rtl';
|
|
143
|
+
const grow = rtl ? 'ArrowLeft' : 'ArrowRight';
|
|
144
|
+
const shrink = rtl ? 'ArrowRight' : 'ArrowLeft';
|
|
145
|
+
const next = event.key === grow
|
|
146
|
+
? shownWidth + KEY_STEP_PX
|
|
147
|
+
: event.key === shrink
|
|
148
|
+
? shownWidth - KEY_STEP_PX
|
|
149
|
+
: event.key === 'Home'
|
|
150
|
+
? minWidth
|
|
151
|
+
: event.key === 'End'
|
|
152
|
+
? maxWidth
|
|
153
|
+
: undefined;
|
|
154
|
+
if (next === undefined)
|
|
155
|
+
return;
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
onWidthChange(clamp(next, minWidth, maxWidth));
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
const overlay = mode === 'overlay';
|
|
161
|
+
const style = {
|
|
162
|
+
position: overlay ? 'absolute' : 'relative',
|
|
163
|
+
insetBlock: overlay ? 0 : undefined,
|
|
164
|
+
insetInlineStart: overlay ? 0 : undefined,
|
|
165
|
+
zIndex: overlay ? 2 : undefined,
|
|
166
|
+
boxShadow: overlay ? 'var(--mantine-shadow-md)' : undefined,
|
|
167
|
+
width: shownWidth,
|
|
168
|
+
maxWidth: '100%',
|
|
169
|
+
height: '100%',
|
|
170
|
+
display: 'flex',
|
|
171
|
+
flexDirection: 'column',
|
|
172
|
+
background: 'var(--mantine-color-body)',
|
|
173
|
+
borderInlineEnd: '1px solid var(--canopy-input-border)',
|
|
174
|
+
};
|
|
175
|
+
return (_jsxs("section", { ref: panelRef, id: panelId, "aria-labelledby": titleId, tabIndex: -1, "data-testid": testId, "data-panel-mode": mode, style: style, onFocus: () => {
|
|
176
|
+
focusInside.current = true;
|
|
177
|
+
}, onBlur: (event) => {
|
|
178
|
+
if (!event.currentTarget.contains(event.relatedTarget))
|
|
179
|
+
focusInside.current = false;
|
|
180
|
+
}, children: [_jsx("style", { children: PANEL_STYLES }), _jsxs(Group, { justify: "space-between", wrap: "nowrap", gap: "xs", px: "md", py: "xs", style: { borderBlockEnd: '1px solid var(--canopy-input-border)' }, children: [_jsx(Text, { id: titleId, component: "h2", fw: 600, size: "md", m: 0, children: title }), _jsxs(Group, { gap: "xs", wrap: "nowrap", children: [actions, _jsx(Tooltip, { label: `Close ${title.toLowerCase()} panel`, children: _jsx(CloseButton, { "aria-label": `Close ${title.toLowerCase()} panel`, onClick: onClose }) })] })] }), _jsx("div", { style: { flex: 1, minHeight: 0, overflow: 'auto' }, children: children }), _jsx("div", { role: "separator", "aria-orientation": "vertical", "aria-label": `Resize ${title.toLowerCase()} panel`, "aria-controls": panelId, "aria-valuenow": Math.round(shownWidth), "aria-valuemin": minWidth, "aria-valuemax": maxWidth,
|
|
181
|
+
// A focusable separator is a window splitter, a widget; jsx-a11y counts every one static.
|
|
182
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
183
|
+
tabIndex: 0, className: "canopy-side-panel-separator", "data-dragging": dragWidth !== null || undefined, ...separatorHandlers })] }));
|
|
184
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
export interface TopBarProps {
|
|
3
|
+
/** Where you are: the breadcrumb and anything that describes the open entry. */
|
|
4
|
+
leading: ReactNode;
|
|
5
|
+
/** What you can do: the branch and the actions on it. */
|
|
6
|
+
trailing: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The shell's top bar: one 48px row from `md` up. Narrower, `leading` takes a row of its own
|
|
10
|
+
* above `trailing`, which wraps rather than clip a control. Layout only.
|
|
11
|
+
*/
|
|
12
|
+
export declare function TopBar({ leading, trailing }: TopBarProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Box, Group } from '@mantine/core';
|
|
4
|
+
const TOP_BAR_HEIGHT = 48;
|
|
5
|
+
/**
|
|
6
|
+
* The shell's top bar: one 48px row from `md` up. Narrower, `leading` takes a row of its own
|
|
7
|
+
* above `trailing`, which wraps rather than clip a control. Layout only.
|
|
8
|
+
*/
|
|
9
|
+
export function TopBar({ leading, trailing }) {
|
|
10
|
+
return (_jsxs(Group, { gap: "xs", wrap: "wrap", align: "center", px: "sm", py: 6, mih: TOP_BAR_HEIGHT, style: {
|
|
11
|
+
borderBottom: '1px solid var(--canopy-input-border)',
|
|
12
|
+
background: 'var(--mantine-color-body)',
|
|
13
|
+
}, children: [_jsx(Box, { flex: { base: '1 0 100%', md: '1 1 0%' }, miw: 0, children: leading }), _jsx(Group, { gap: 6, wrap: "wrap", align: "center", justify: "flex-end", flex: { base: '1 0 100%', md: '0 1 auto' }, miw: 0, children: trailing })] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* @internal Exported for tests. The host page's chrome around `anchor`, an element the editor
|
|
4
|
+
* renders in place: the `<body>` child holding it, plus every `<body>` child the same copy of React
|
|
5
|
+
* rendered (a Next.js root layout's header and footer). That copy's fiber key has a random suffix,
|
|
6
|
+
* so portal containers and the roots other libraries create, such as MDXEditor's popup
|
|
7
|
+
* container, never match it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function findHostChrome(anchor: Element): HTMLElement[];
|
|
10
|
+
/**
|
|
11
|
+
* Makes the host's chrome inert and hidden while the editor is mounted: Tab never leaves the
|
|
12
|
+
* editor, and chrome with its own z-index never paints over the shell. Applied once, at mount,
|
|
13
|
+
* since an element added later may be a third-party popup that must stay usable; unmount restores
|
|
14
|
+
* exactly what this changed.
|
|
15
|
+
*/
|
|
16
|
+
export declare function useHideHostChrome(anchorRef: RefObject<Element | null>): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
const REACT_FIBER_KEY_PREFIX = '__reactFiber$';
|
|
4
|
+
function reactFiberKey(element) {
|
|
5
|
+
return Object.keys(element).find((key) => key.startsWith(REACT_FIBER_KEY_PREFIX));
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* @internal Exported for tests. The host page's chrome around `anchor`, an element the editor
|
|
9
|
+
* renders in place: the `<body>` child holding it, plus every `<body>` child the same copy of React
|
|
10
|
+
* rendered (a Next.js root layout's header and footer). That copy's fiber key has a random suffix,
|
|
11
|
+
* so portal containers and the roots other libraries create, such as MDXEditor's popup
|
|
12
|
+
* container, never match it.
|
|
13
|
+
*/
|
|
14
|
+
export function findHostChrome(anchor) {
|
|
15
|
+
const body = anchor.ownerDocument.body;
|
|
16
|
+
let root = anchor;
|
|
17
|
+
while (root.parentElement && root.parentElement !== body)
|
|
18
|
+
root = root.parentElement;
|
|
19
|
+
if (root.parentElement !== body)
|
|
20
|
+
return [];
|
|
21
|
+
const fiberKey = reactFiberKey(anchor);
|
|
22
|
+
const siblings = fiberKey
|
|
23
|
+
? Array.from(body.children).filter((child) => child !== root && fiberKey in child)
|
|
24
|
+
: [];
|
|
25
|
+
return [root, ...siblings].filter((element) => element instanceof HTMLElement);
|
|
26
|
+
}
|
|
27
|
+
/** Per element, so a second editor mounted at once never restores what the first still hides. */
|
|
28
|
+
const holds = new WeakMap();
|
|
29
|
+
function hide(element) {
|
|
30
|
+
let hold = holds.get(element);
|
|
31
|
+
if (!hold) {
|
|
32
|
+
const wasInert = element.hasAttribute('inert');
|
|
33
|
+
const { visibility } = element.style;
|
|
34
|
+
element.setAttribute('inert', '');
|
|
35
|
+
element.style.visibility = 'hidden';
|
|
36
|
+
hold = {
|
|
37
|
+
count: 0,
|
|
38
|
+
restore: () => {
|
|
39
|
+
if (!wasInert)
|
|
40
|
+
element.removeAttribute('inert');
|
|
41
|
+
element.style.visibility = visibility;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
holds.set(element, hold);
|
|
45
|
+
}
|
|
46
|
+
hold.count++;
|
|
47
|
+
const held = hold;
|
|
48
|
+
return () => {
|
|
49
|
+
if (--held.count > 0)
|
|
50
|
+
return;
|
|
51
|
+
holds.delete(element);
|
|
52
|
+
held.restore();
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Makes the host's chrome inert and hidden while the editor is mounted: Tab never leaves the
|
|
57
|
+
* editor, and chrome with its own z-index never paints over the shell. Applied once, at mount,
|
|
58
|
+
* since an element added later may be a third-party popup that must stay usable; unmount restores
|
|
59
|
+
* exactly what this changed.
|
|
60
|
+
*/
|
|
61
|
+
export function useHideHostChrome(anchorRef) {
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
const anchor = anchorRef.current;
|
|
64
|
+
if (!anchor)
|
|
65
|
+
return;
|
|
66
|
+
const releases = findHostChrome(anchor).map(hide);
|
|
67
|
+
return () => releases.forEach((release) => release());
|
|
68
|
+
}, [anchorRef]);
|
|
69
|
+
}
|
package/dist/editor/theme.js
CHANGED
|
@@ -104,7 +104,18 @@ const canopyComponents = {
|
|
|
104
104
|
}
|
|
105
105
|
: { root: {} },
|
|
106
106
|
}),
|
|
107
|
-
ActionIcon: ActionIcon.extend({
|
|
107
|
+
ActionIcon: ActionIcon.extend({
|
|
108
|
+
defaultProps: { size: 'sm' },
|
|
109
|
+
vars: (theme, props) => (props.variant ?? 'filled') === 'filled' && isPrimary(theme, props.color)
|
|
110
|
+
? {
|
|
111
|
+
root: {
|
|
112
|
+
'--ai-bg': 'var(--canopy-primary-fill)',
|
|
113
|
+
'--ai-hover': 'var(--canopy-primary-fill-hover)',
|
|
114
|
+
'--ai-color': 'var(--mantine-color-white)',
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
: { root: {} },
|
|
118
|
+
}),
|
|
108
119
|
// Tooltips open on focus and touch too, so an icon-only control's name reaches the keyboard.
|
|
109
120
|
Tooltip: Tooltip.extend({ defaultProps: { events: { hover: true, focus: true, touch: true } } }),
|
|
110
121
|
// A default input's border is `--canopy-input-border`; errored and focused ones keep Mantine's.
|
|
@@ -151,7 +162,11 @@ const canopyComponents = {
|
|
|
151
162
|
}),
|
|
152
163
|
ModalCloseButton: Modal.CloseButton.extend({ defaultProps: CLOSE_BUTTON_PROPS }),
|
|
153
164
|
DrawerCloseButton: Drawer.CloseButton.extend({ defaultProps: CLOSE_BUTTON_PROPS }),
|
|
154
|
-
|
|
165
|
+
// Mantine dims a titled toast's text to gray.6 / dark.2, under 4.5:1 on its surface.
|
|
166
|
+
Notification: Notification.extend({
|
|
167
|
+
defaultProps: { closeButtonProps: CLOSE_BUTTON_PROPS },
|
|
168
|
+
styles: { description: { color: 'var(--mantine-color-text)' } },
|
|
169
|
+
}),
|
|
155
170
|
// A light yellow Alert's title, icon and close button are yellow on pale yellow, under 2:1, so
|
|
156
171
|
// they take the text colour; the tint and the icon's shape still mark it a warning.
|
|
157
172
|
Alert: Alert.extend({
|
|
@@ -348,5 +363,5 @@ input.mantine-TimeInput-input::-webkit-datetime-edit-ampm-field:focus {
|
|
|
348
363
|
`;
|
|
349
364
|
export const CanopyCMSProvider = ({ children, colors, colorScheme = 'light', themeOverride, withNotifications = true, }) => {
|
|
350
365
|
const theme = useMemo(() => createCanopyTheme({ colors, themeOverride }), [colors, themeOverride]);
|
|
351
|
-
return (_jsxs(MantineProvider, { theme: theme, defaultColorScheme: colorScheme, cssVariablesResolver: canopyCssVariablesResolver, children: [_jsx("style", { "data-canopy-styles": true, dangerouslySetInnerHTML: { __html: CANOPY_STYLES } }), _jsxs(ModalsProvider, { modalProps: { zIndex: getDefaultZIndex('popover') }, children: [withNotifications ? _jsx(Notifications, { position: "bottom-
|
|
366
|
+
return (_jsxs(MantineProvider, { theme: theme, defaultColorScheme: colorScheme, cssVariablesResolver: canopyCssVariablesResolver, children: [_jsx("style", { "data-canopy-styles": true, dangerouslySetInnerHTML: { __html: CANOPY_STYLES } }), _jsxs(ModalsProvider, { modalProps: { zIndex: getDefaultZIndex('popover') }, children: [withNotifications ? _jsx(Notifications, { position: "bottom-right" }) : null, children] })] }));
|
|
352
367
|
};
|
package/dist/git-manager.d.ts
CHANGED
|
@@ -54,7 +54,7 @@ export declare function repackBareRemoteIfNeeded(gitDir: string, git?: SimpleGit
|
|
|
54
54
|
* so a clone or checkout the OOM killer stopped would pass for complete. Given as `errors` to a
|
|
55
55
|
* GitManager's own instance, to `cloneRepo`'s, and to the worker's ref probes of an existing
|
|
56
56
|
* remote.git (worker/cms-worker.ts); its static helpers and the bare-remote functions do not pass
|
|
57
|
-
* it yet (
|
|
57
|
+
* it yet (safeinsights/canopycms#845).
|
|
58
58
|
*/
|
|
59
59
|
export declare function failOnSignalExit(error: Buffer | Error | undefined, result: {
|
|
60
60
|
exitCode: number;
|
package/dist/git-manager.js
CHANGED
|
@@ -181,7 +181,7 @@ export async function repackBareRemoteIfNeeded(gitDir, git = simpleGit().env(git
|
|
|
181
181
|
* so a clone or checkout the OOM killer stopped would pass for complete. Given as `errors` to a
|
|
182
182
|
* GitManager's own instance, to `cloneRepo`'s, and to the worker's ref probes of an existing
|
|
183
183
|
* remote.git (worker/cms-worker.ts); its static helpers and the bare-remote functions do not pass
|
|
184
|
-
* it yet (
|
|
184
|
+
* it yet (safeinsights/canopycms#845).
|
|
185
185
|
*/
|
|
186
186
|
export function failOnSignalExit(error, result) {
|
|
187
187
|
if (error)
|
|
@@ -399,7 +399,7 @@ export class GitManager {
|
|
|
399
399
|
// gitNetworkChildEnv instead. Under the `allowNetworkRemoteInProd` escape
|
|
400
400
|
// hatch `this.remote` CAN be a network URL, and these calls do hit it with
|
|
401
401
|
// that restricted env: a known limitation of the escape hatch, tracked in
|
|
402
|
-
//
|
|
402
|
+
// safeinsights/canopycms#754.
|
|
403
403
|
//
|
|
404
404
|
// GIT_CEILING_DIRECTORIES stops git traversing above repoPath to a parent
|
|
405
405
|
// .git: a corrupt or missing workspace .git must fail, never silently
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { OperatingMode as OM } from './index.js';
|
|
3
3
|
import type { CanopyConfig } from '../config/index.js';
|
|
4
4
|
export type OperatingMode = OM;
|
|
5
|
-
/** @internal No importer; deletion candidate in
|
|
5
|
+
/** @internal No importer; deletion candidate in safeinsights/canopycms#725. */
|
|
6
6
|
export interface ResolveRemoteUrlOptions {
|
|
7
7
|
mode: OperatingMode;
|
|
8
8
|
remoteUrl?: string;
|
|
@@ -3,7 +3,7 @@ import type { CanopyUser } from './user.js';
|
|
|
3
3
|
import type { OperatingMode } from './operating-mode/index.js';
|
|
4
4
|
/**
|
|
5
5
|
* Test-only: reset the once-per-process "no admins configured" warning latch.
|
|
6
|
-
* @internal No importer; deletion candidate in
|
|
6
|
+
* @internal No importer; deletion candidate in safeinsights/canopycms#725.
|
|
7
7
|
*/
|
|
8
8
|
export declare function resetResolveCanopyUserWarningForTests(): void;
|
|
9
9
|
export interface ResolveCanopyUserDeps {
|
|
@@ -17,7 +17,7 @@ import { timeRequestPhase } from './utils/request-timing.js';
|
|
|
17
17
|
let warnedNoAdmins = false;
|
|
18
18
|
/**
|
|
19
19
|
* Test-only: reset the once-per-process "no admins configured" warning latch.
|
|
20
|
-
* @internal No importer; deletion candidate in
|
|
20
|
+
* @internal No importer; deletion candidate in safeinsights/canopycms#725.
|
|
21
21
|
*/
|
|
22
22
|
export function resetResolveCanopyUserWarningForTests() {
|
|
23
23
|
warnedNoAdmins = false;
|
|
@@ -26,7 +26,7 @@ export declare function resolveSchema(contentRoot: string, entrySchemaRegistry:
|
|
|
26
26
|
*
|
|
27
27
|
* @param contentRoot - Path to the content directory
|
|
28
28
|
* @returns true if at least one .collection.json file exists
|
|
29
|
-
* @internal No importer; deletion candidate in
|
|
29
|
+
* @internal No importer; deletion candidate in safeinsights/canopycms#725.
|
|
30
30
|
*/
|
|
31
31
|
export declare function hasSchemaFiles(contentRoot: string): Promise<boolean>;
|
|
32
32
|
/**
|
package/dist/schema/resolver.js
CHANGED
|
@@ -43,7 +43,7 @@ export async function resolveSchema(contentRoot, entrySchemaRegistry, options =
|
|
|
43
43
|
*
|
|
44
44
|
* @param contentRoot - Path to the content directory
|
|
45
45
|
* @returns true if at least one .collection.json file exists
|
|
46
|
-
* @internal No importer; deletion candidate in
|
|
46
|
+
* @internal No importer; deletion candidate in safeinsights/canopycms#725.
|
|
47
47
|
*/
|
|
48
48
|
export async function hasSchemaFiles(contentRoot) {
|
|
49
49
|
const metaFiles = await loadCollectionMetaFiles(contentRoot);
|
|
@@ -317,7 +317,7 @@ export declare class SchemaOps {
|
|
|
317
317
|
* root can re-normalise to the ROOT collection on a second call, mutating
|
|
318
318
|
* the wrong one instead of reporting not-found. Every entry point calls
|
|
319
319
|
* this exactly once today, so the ambiguity is unreachable; see
|
|
320
|
-
*
|
|
320
|
+
* safeinsights/canopycms#623. A future
|
|
321
321
|
* method that accepts a logical collection path must do the same.
|
|
322
322
|
*/
|
|
323
323
|
private normalizeCollectionPath;
|
|
@@ -304,7 +304,7 @@ export class SchemaOps {
|
|
|
304
304
|
* root can re-normalise to the ROOT collection on a second call, mutating
|
|
305
305
|
* the wrong one instead of reporting not-found. Every entry point calls
|
|
306
306
|
* this exactly once today, so the ambiguity is unreachable; see
|
|
307
|
-
*
|
|
307
|
+
* safeinsights/canopycms#623. A future
|
|
308
308
|
* method that accepts a logical collection path must do the same.
|
|
309
309
|
*/
|
|
310
310
|
normalizeCollectionPath(collectionPath) {
|
|
@@ -580,7 +580,7 @@ export class SchemaOps {
|
|
|
580
580
|
// already rules out a `{slug}.{id}` directory at the destination — but that check is
|
|
581
581
|
// case-SENSITIVE and lets an ID-less `docs/`-style directory through, so a case or
|
|
582
582
|
// ID-less collision can still slip past it for entries deeper than the index; see
|
|
583
|
-
//
|
|
583
|
+
// safeinsights/canopycms#624.
|
|
584
584
|
if (await findIndexEntryIn(physicalPath)) {
|
|
585
585
|
// Renaming TO "index" collapses this collection onto its OWN new path, not the
|
|
586
586
|
// parent's — see url-collision.ts's findUrlPathClaimant for the equivalent create-time
|
package/dist/url-collision.js
CHANGED
|
@@ -18,7 +18,7 @@ import { parseTypedFilename } from './utils/typed-filename.js';
|
|
|
18
18
|
* `doc.index.{id}.json` inside an md-only collection claims no URL yet still blocks a sibling
|
|
19
19
|
* write. Closing it means threading each collection's configured formats into a deliberately
|
|
20
20
|
* schema-free module; tracked in
|
|
21
|
-
*
|
|
21
|
+
* safeinsights/canopycms#878.
|
|
22
22
|
*
|
|
23
23
|
* A literal rather than derived: deriving it needs the `ContentFormat` union this module has no
|
|
24
24
|
* reason to import. `url-collision.test.ts` carries the drift tripwire — a compile-time
|
|
@@ -65,7 +65,7 @@ async function findChildCollectionDir(dir, name) {
|
|
|
65
65
|
*
|
|
66
66
|
* The claimant set is nonetheless a strict superset of the listing's in three ways, all
|
|
67
67
|
* over-blocking and all needing the same fix — the schema this module deliberately does not have
|
|
68
|
-
* — so they are one task (
|
|
68
|
+
* — so they are one task (safeinsights/canopycms#878):
|
|
69
69
|
*
|
|
70
70
|
* 1. extension — see `CONTENT_EXTENSIONS` above;
|
|
71
71
|
* 2. entry TYPE — parsing is structural (no `entryTypes` list), so a file whose type is not in
|
|
@@ -155,7 +155,7 @@ export async function findUrlPathClaimant(opts) {
|
|
|
155
155
|
// KNOWN DEPENDENCY: this resolves ONE child collection by name, and the index direction above
|
|
156
156
|
// likewise looks only for a parent ENTRY. Neither considers a second collection sharing the same
|
|
157
157
|
// name, because nothing stops one being created -- `createCollectionInner` does not check
|
|
158
|
-
// sibling names (tracked in
|
|
158
|
+
// sibling names (tracked in safeinsights/canopycms#624). With two `guides.{id}`
|
|
159
159
|
// directories side by side, an index entry can be written into each: both writes pass this
|
|
160
160
|
// guard, two entries claim one URL, and only the build guard catches it. Fixing sibling-name
|
|
161
161
|
// uniqueness closes this without changing anything here, so that task is a prerequisite for
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Scope of the invariant it checks: entries written in the `{type}.{slug}.{id}.{ext}` grammar
|
|
16
16
|
* with a type their collection declares -- exactly the set `listEntries` can see. A legacy
|
|
17
17
|
* untyped file (`overview.json`) is invisible to enumeration and is deliberately NOT probed here;
|
|
18
|
-
* see
|
|
18
|
+
* see safeinsights/canopycms#728.
|
|
19
19
|
*/
|
|
20
20
|
import { type RootCollectionConfig } from './config/index.js';
|
|
21
21
|
import type { ListEntriesItem } from './content-listing.js';
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Scope of the invariant it checks: entries written in the `{type}.{slug}.{id}.{ext}` grammar
|
|
16
16
|
* with a type their collection declares -- exactly the set `listEntries` can see. A legacy
|
|
17
17
|
* untyped file (`overview.json`) is invisible to enumeration and is deliberately NOT probed here;
|
|
18
|
-
* see
|
|
18
|
+
* see safeinsights/canopycms#728.
|
|
19
19
|
*/
|
|
20
20
|
import { flattenSchema } from './config/index.js';
|
|
21
21
|
import { findDuplicateUrlPaths } from './static/index.js';
|
|
@@ -44,7 +44,7 @@ import { type OnLockCompromised } from './provisioning-lock.js';
|
|
|
44
44
|
* `next dev` and build-time provisioning across worker processes. The case to watch is a write
|
|
45
45
|
* whose in-lock path triggers a full `idIndex()` rescan of a large tree over EFS, which can exceed
|
|
46
46
|
* this budget and start 409ing unrelated saves. Making it configurable is tracked in
|
|
47
|
-
*
|
|
47
|
+
* safeinsights/canopycms#640.
|
|
48
48
|
*/
|
|
49
49
|
export declare const DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS = 2000;
|
|
50
50
|
/**
|
|
@@ -52,7 +52,7 @@ const CONTENT_WRITE_LOCK_NAME = 'content-write.lock';
|
|
|
52
52
|
* `next dev` and build-time provisioning across worker processes. The case to watch is a write
|
|
53
53
|
* whose in-lock path triggers a full `idIndex()` rescan of a large tree over EFS, which can exceed
|
|
54
54
|
* this budget and start 409ing unrelated saves. Making it configurable is tracked in
|
|
55
|
-
*
|
|
55
|
+
* safeinsights/canopycms#640.
|
|
56
56
|
*/
|
|
57
57
|
export const DEFAULT_CONTENT_WRITE_LOCK_WAIT_MS = 2000;
|
|
58
58
|
/**
|
package/dist/utils/debug.d.ts
CHANGED
|
@@ -41,6 +41,6 @@ export declare class DebugLogger {
|
|
|
41
41
|
export declare function createDebugLogger(options?: DebugOptions): DebugLogger;
|
|
42
42
|
/**
|
|
43
43
|
* Default logger for E2E test infrastructure.
|
|
44
|
-
* @internal No importer; deletion candidate in
|
|
44
|
+
* @internal No importer; deletion candidate in safeinsights/canopycms#725.
|
|
45
45
|
*/
|
|
46
46
|
export declare const testLogger: DebugLogger;
|
package/dist/utils/debug.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/** False where no `process` exists: a browser bundle with no shim for it, such as Storybook's. */
|
|
2
|
+
const envFlag = (name) => typeof process !== 'undefined' && process.env[name] === 'true';
|
|
1
3
|
const LOG_LEVELS = {
|
|
2
4
|
DEBUG: 0,
|
|
3
5
|
INFO: 1,
|
|
@@ -11,7 +13,7 @@ export class DebugLogger {
|
|
|
11
13
|
}
|
|
12
14
|
shouldLog(level) {
|
|
13
15
|
// Check enabled at call time to support runtime env var changes
|
|
14
|
-
const enabled = this.options.enabled ??
|
|
16
|
+
const enabled = this.options.enabled ?? envFlag('CANOPYCMS_DEBUG');
|
|
15
17
|
if (!enabled)
|
|
16
18
|
return false;
|
|
17
19
|
const minLevel = this.options.minLevel ?? 'DEBUG';
|
|
@@ -92,10 +94,10 @@ export function createDebugLogger(options) {
|
|
|
92
94
|
}
|
|
93
95
|
/**
|
|
94
96
|
* Default logger for E2E test infrastructure.
|
|
95
|
-
* @internal No importer; deletion candidate in
|
|
97
|
+
* @internal No importer; deletion candidate in safeinsights/canopycms#725.
|
|
96
98
|
*/
|
|
97
99
|
export const testLogger = createDebugLogger({
|
|
98
|
-
enabled:
|
|
100
|
+
enabled: envFlag('E2E_DEBUG'),
|
|
99
101
|
prefix: 'E2E',
|
|
100
102
|
throwOnError: false,
|
|
101
103
|
});
|
package/dist/utils/git.d.ts
CHANGED
|
@@ -47,6 +47,10 @@ export declare function isStaleLeaseRejection(message: string): boolean;
|
|
|
47
47
|
* GitHub refuses only content it does not already hold: carrying a base-branch workflow change by
|
|
48
48
|
* rebase, merge or fast-forward is accepted. Retrying the identical push can never succeed, so the
|
|
49
49
|
* worker fails it fast. The text is GitHub's own, not git's, so no locale pinning is needed.
|
|
50
|
+
*
|
|
51
|
+
* A scan of each line's backquote-delimited spans, linear in the message, which is git's output of
|
|
52
|
+
* any length: this rule as a regex, with its run between lead and tail, is quadratic on a line
|
|
53
|
+
* that repeats the lead without the tail.
|
|
50
54
|
*/
|
|
51
55
|
export declare function workflowPushRefusalFile(message: string): string | null;
|
|
52
56
|
/**
|
package/dist/utils/git.js
CHANGED
|
@@ -90,10 +90,11 @@ const STALE_LEASE_REASON = 'stale info';
|
|
|
90
90
|
export function isStaleLeaseRejection(message) {
|
|
91
91
|
return message.includes(REJECTED_MARKER) && message.includes(STALE_LEASE_REASON);
|
|
92
92
|
}
|
|
93
|
-
// GitHub's reason text when a push would introduce workflow content the credential may not write
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
const
|
|
93
|
+
// GitHub's reason text when a push would introduce workflow content the credential may not write:
|
|
94
|
+
// `refusing to allow <credential kind> to create or update workflow `<file>``, where the kind
|
|
95
|
+
// ("a GitHub App", "an OAuth App", ...) holds no backquote and the file is the next quoted span.
|
|
96
|
+
const WORKFLOW_REFUSAL_LEADS = ['refusing to allow a ', 'refusing to allow an '];
|
|
97
|
+
const WORKFLOW_REFUSAL_TAIL = ' to create or update workflow ';
|
|
97
98
|
/**
|
|
98
99
|
* The workflow file named by GitHub's refusal of a push that would add workflow content the
|
|
99
100
|
* credential lacks the workflows permission for, or null if the message is not that refusal.
|
|
@@ -101,9 +102,30 @@ const WORKFLOW_REFUSAL_PATTERN = /refusing to allow an? [^`\n]+? to create or up
|
|
|
101
102
|
* GitHub refuses only content it does not already hold: carrying a base-branch workflow change by
|
|
102
103
|
* rebase, merge or fast-forward is accepted. Retrying the identical push can never succeed, so the
|
|
103
104
|
* worker fails it fast. The text is GitHub's own, not git's, so no locale pinning is needed.
|
|
105
|
+
*
|
|
106
|
+
* A scan of each line's backquote-delimited spans, linear in the message, which is git's output of
|
|
107
|
+
* any length: this rule as a regex, with its run between lead and tail, is quadratic on a line
|
|
108
|
+
* that repeats the lead without the tail.
|
|
104
109
|
*/
|
|
105
110
|
export function workflowPushRefusalFile(message) {
|
|
106
|
-
|
|
111
|
+
for (const line of message.split('\n')) {
|
|
112
|
+
const spans = line.split('`');
|
|
113
|
+
// The last span is closed by no backquote, so the file can be at most the one before it.
|
|
114
|
+
for (let i = 0; i + 2 < spans.length; i++) {
|
|
115
|
+
const span = spans[i];
|
|
116
|
+
const file = spans[i + 1];
|
|
117
|
+
if (file === '' || !span.endsWith(WORKFLOW_REFUSAL_TAIL))
|
|
118
|
+
continue;
|
|
119
|
+
const head = span.slice(0, -WORKFLOW_REFUSAL_TAIL.length);
|
|
120
|
+
const named = WORKFLOW_REFUSAL_LEADS.some((lead) => {
|
|
121
|
+
const at = head.indexOf(lead);
|
|
122
|
+
return at !== -1 && at + lead.length < head.length;
|
|
123
|
+
});
|
|
124
|
+
if (named)
|
|
125
|
+
return file;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
107
129
|
}
|
|
108
130
|
// git's message when `git fetch <remote> <branch>` names a ref the remote does not have. Both
|
|
109
131
|
// spellings occur: modern git prints the lowercase form, older versions and some transports
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* Two other readers still spell the keys out themselves — they read the value
|
|
17
17
|
* positionally and disagree on precedence (`resolveBlockItem` prefers `template`,
|
|
18
18
|
* `ai/json-to-markdown.ts` prefers `_type`) — see
|
|
19
|
-
*
|
|
19
|
+
* safeinsights/canopycms#584.
|
|
20
20
|
*
|
|
21
21
|
* Dependency-free on purpose: both importers reach contexts that must not pull in
|
|
22
22
|
* schema types or node built-ins.
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* Two other readers still spell the keys out themselves — they read the value
|
|
17
17
|
* positionally and disagree on precedence (`resolveBlockItem` prefers `template`,
|
|
18
18
|
* `ai/json-to-markdown.ts` prefers `_type`) — see
|
|
19
|
-
*
|
|
19
|
+
* safeinsights/canopycms#584.
|
|
20
20
|
*
|
|
21
21
|
* Dependency-free on purpose: both importers reach contexts that must not pull in
|
|
22
22
|
* schema types or node built-ins.
|