@rigel-cms/rigel 1.0.0 → 1.2.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/README.md +8 -0
- package/docs/content-locales.md +53 -0
- package/docs/extensions.md +194 -0
- package/package.json +16 -3
- package/src/article-body.ts +49 -37
- package/src/article-render.ts +25 -2
- package/src/article.ts +44 -0
- package/src/config.ts +26 -2
- package/src/editor/App.svelte +149 -107
- package/src/editor/blocks/extension.ts +29 -0
- package/src/editor/blocks/index.ts +17 -15
- package/src/editor/blocks/video.ts +2 -1
- package/src/editor/i18n/README.md +23 -0
- package/src/editor/i18n/de/connection.json +136 -0
- package/src/editor/i18n/de/defaults.json +6 -0
- package/src/editor/i18n/de/fields.json +130 -0
- package/src/editor/i18n/de/local.json +49 -0
- package/src/editor/i18n/de/media.json +166 -0
- package/src/editor/i18n/de/richtext.json +116 -0
- package/src/editor/i18n/de/shell.json +182 -0
- package/src/editor/i18n/de/system.json +14 -0
- package/src/editor/i18n/de/validation.json +82 -0
- package/src/editor/i18n/en/connection.json +136 -0
- package/src/editor/i18n/en/defaults.json +6 -0
- package/src/editor/i18n/en/fields.json +130 -0
- package/src/editor/i18n/en/local.json +49 -0
- package/src/editor/i18n/en/media.json +166 -0
- package/src/editor/i18n/en/richtext.json +116 -0
- package/src/editor/i18n/en/shell.json +182 -0
- package/src/editor/i18n/en/system.json +14 -0
- package/src/editor/i18n/en/validation.json +82 -0
- package/src/editor/i18n/es/connection.json +136 -0
- package/src/editor/i18n/es/defaults.json +6 -0
- package/src/editor/i18n/es/fields.json +130 -0
- package/src/editor/i18n/es/local.json +49 -0
- package/src/editor/i18n/es/media.json +166 -0
- package/src/editor/i18n/es/richtext.json +116 -0
- package/src/editor/i18n/es/shell.json +182 -0
- package/src/editor/i18n/es/system.json +14 -0
- package/src/editor/i18n/es/validation.json +82 -0
- package/src/editor/i18n/format.ts +26 -0
- package/src/editor/i18n/fr/connection.json +136 -0
- package/src/editor/i18n/fr/defaults.json +6 -0
- package/src/editor/i18n/fr/fields.json +130 -0
- package/src/editor/i18n/fr/local.json +49 -0
- package/src/editor/i18n/fr/media.json +166 -0
- package/src/editor/i18n/fr/richtext.json +116 -0
- package/src/editor/i18n/fr/shell.json +182 -0
- package/src/editor/i18n/fr/system.json +14 -0
- package/src/editor/i18n/fr/validation.json +82 -0
- package/src/editor/i18n/index.ts +18 -0
- package/src/editor/i18n/it/connection.json +136 -0
- package/src/editor/i18n/it/defaults.json +6 -0
- package/src/editor/i18n/it/fields.json +130 -0
- package/src/editor/i18n/it/local.json +49 -0
- package/src/editor/i18n/it/media.json +166 -0
- package/src/editor/i18n/it/richtext.json +116 -0
- package/src/editor/i18n/it/shell.json +182 -0
- package/src/editor/i18n/it/system.json +14 -0
- package/src/editor/i18n/it/validation.json +82 -0
- package/src/editor/i18n/ja/connection.json +136 -0
- package/src/editor/i18n/ja/defaults.json +6 -0
- package/src/editor/i18n/ja/fields.json +130 -0
- package/src/editor/i18n/ja/local.json +49 -0
- package/src/editor/i18n/ja/media.json +166 -0
- package/src/editor/i18n/ja/richtext.json +116 -0
- package/src/editor/i18n/ja/shell.json +182 -0
- package/src/editor/i18n/ja/system.json +14 -0
- package/src/editor/i18n/ja/validation.json +82 -0
- package/src/editor/i18n/locales.ts +13 -0
- package/src/editor/i18n/nl/connection.json +136 -0
- package/src/editor/i18n/nl/defaults.json +6 -0
- package/src/editor/i18n/nl/fields.json +130 -0
- package/src/editor/i18n/nl/local.json +49 -0
- package/src/editor/i18n/nl/media.json +166 -0
- package/src/editor/i18n/nl/richtext.json +116 -0
- package/src/editor/i18n/nl/shell.json +182 -0
- package/src/editor/i18n/nl/system.json +14 -0
- package/src/editor/i18n/nl/validation.json +82 -0
- package/src/editor/i18n/ru/connection.json +138 -0
- package/src/editor/i18n/ru/defaults.json +6 -0
- package/src/editor/i18n/ru/fields.json +132 -0
- package/src/editor/i18n/ru/local.json +49 -0
- package/src/editor/i18n/ru/media.json +166 -0
- package/src/editor/i18n/ru/richtext.json +116 -0
- package/src/editor/i18n/ru/shell.json +182 -0
- package/src/editor/i18n/ru/system.json +14 -0
- package/src/editor/i18n/ru/validation.json +82 -0
- package/src/editor/i18n/server.ts +13 -0
- package/src/editor/i18n/zh/connection.json +136 -0
- package/src/editor/i18n/zh/defaults.json +6 -0
- package/src/editor/i18n/zh/fields.json +130 -0
- package/src/editor/i18n/zh/local.json +49 -0
- package/src/editor/i18n/zh/media.json +166 -0
- package/src/editor/i18n/zh/richtext.json +116 -0
- package/src/editor/i18n/zh/shell.json +182 -0
- package/src/editor/i18n/zh/system.json +14 -0
- package/src/editor/i18n/zh/validation.json +82 -0
- package/src/editor/lib/ActionDialog.svelte +5 -4
- package/src/editor/lib/BlockEditor.svelte +305 -88
- package/src/editor/lib/CollectionPanel.svelte +17 -15
- package/src/editor/lib/DatePicker.svelte +15 -12
- package/src/editor/lib/ExtensionMount.svelte +12 -0
- package/src/editor/lib/Field.svelte +3 -2
- package/src/editor/lib/ImagePicker.svelte +82 -81
- package/src/editor/lib/MarkdownEditor.svelte +25 -24
- package/src/editor/lib/MediaPicker.svelte +6 -5
- package/src/editor/lib/MenuEditor.svelte +31 -30
- package/src/editor/lib/PreviewFrame.svelte +11 -6
- package/src/editor/lib/PropertiesTabs.svelte +3 -2
- package/src/editor/lib/PublishDialog.svelte +15 -14
- package/src/editor/lib/ReferenceSelect.svelte +3 -2
- package/src/editor/lib/SchemaField.svelte +15 -13
- package/src/editor/lib/SchemaPanel.svelte +20 -19
- package/src/editor/lib/Select.svelte +5 -4
- package/src/editor/lib/SeoGuidance.svelte +6 -5
- package/src/editor/lib/Settings.svelte +54 -54
- package/src/editor/lib/Shortcuts.svelte +7 -6
- package/src/editor/lib/SitePreview.svelte +10 -9
- package/src/editor/lib/TopBar.svelte +18 -17
- package/src/editor/lib/UnsavedChanges.svelte +7 -6
- package/src/editor/lib/VisualEditor.svelte +19 -18
- package/src/editor/lib/calendar.ts +10 -4
- package/src/editor/lib/embed-preview.ts +23 -5
- package/src/editor/lib/extension-preview.ts +121 -0
- package/src/editor/lib/github-auth.ts +23 -22
- package/src/editor/lib/github-backend.ts +96 -91
- package/src/editor/lib/images.ts +33 -31
- package/src/editor/lib/menu-tree.ts +5 -4
- package/src/editor/lib/preview-frame.ts +4 -3
- package/src/editor/lib/schema-edit.ts +3 -2
- package/src/editor/lib/settings.ts +5 -3
- package/src/editor/lib/slash-command.ts +1 -0
- package/src/editor/lib/tooltip.ts +1 -1
- package/src/editor/main.ts +9 -0
- package/src/editor/styles.css +15 -3
- package/src/env.d.ts +11 -1
- package/src/extensions.ts +160 -0
- package/src/index.ts +1 -1
- package/src/manifest/collections.ts +6 -5
- package/src/manifest/content-locales.ts +45 -0
- package/src/manifest/menu.ts +12 -10
- package/src/manifest/navigation.ts +13 -8
- package/src/manifest/references.ts +8 -7
- package/src/manifest/seo.ts +4 -3
- package/src/manifest/types.ts +4 -0
- package/src/preview.ts +36 -2
- package/src/routes/local-api.ts +5 -3
- package/src/routes/local-documents.ts +20 -16
- package/src/routes/local-media.ts +21 -16
- package/src/routes/local-menu.ts +9 -7
- package/src/routes/local-post.ts +18 -12
- package/src/routes/media-files.ts +10 -8
- package/src/routes/rigel-page.astro +2 -1
- package/src/schema/store.ts +22 -20
- package/src/schema/types.ts +2 -0
- package/src/schema/validate.ts +73 -69
- package/src/slugs.ts +11 -10
- package/src/vite/manifest-plugin.ts +38 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { t } from "../i18n";
|
|
2
3
|
import { mount, unmount, onDestroy, tick, type Snippet } from "svelte";
|
|
3
4
|
import BlockEditor from "./BlockEditor.svelte";
|
|
4
5
|
import { tooltipRoot } from "./tooltip";
|
|
@@ -93,7 +94,7 @@
|
|
|
93
94
|
|
|
94
95
|
function anchorSelector(path: string) {
|
|
95
96
|
const escaped = CSS.escape(path);
|
|
96
|
-
return `[data-rigel-text="${escaped}"],[data-rigel-richtext="${escaped}"],[data-rigel-image="${escaped}"],[data-rigel-alt="${escaped}"],[data-rigel-href="${escaped}"]`;
|
|
97
|
+
return `[data-rigel-tags="${escaped}"],[data-rigel-text="${escaped}"],[data-rigel-richtext="${escaped}"],[data-rigel-image="${escaped}"],[data-rigel-alt="${escaped}"],[data-rigel-href="${escaped}"]`;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
function applySelection(scroll = false) {
|
|
@@ -165,7 +166,7 @@
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
function fieldPathFrom(element: HTMLElement): { path: string; doc: string } | null {
|
|
168
|
-
for (const attr of ["data-rigel-text", "data-rigel-richtext", "data-rigel-image", "data-rigel-alt", "data-rigel-href"]) {
|
|
169
|
+
for (const attr of ["data-rigel-tags", "data-rigel-text", "data-rigel-richtext", "data-rigel-image", "data-rigel-alt", "data-rigel-href"]) {
|
|
169
170
|
const value = element.getAttribute(attr);
|
|
170
171
|
if (value) return { path: value, doc: element.getAttribute("data-rigel-doc") ?? docId };
|
|
171
172
|
}
|
|
@@ -263,7 +264,7 @@
|
|
|
263
264
|
if (!target) return;
|
|
264
265
|
const articleBody = target.closest<HTMLElement>('[data-rigel-richtext="$body"]');
|
|
265
266
|
if (inlineBodyEnabled && articleBody) { void startBodyEdit(articleBody, event); return; }
|
|
266
|
-
const element = target.closest<HTMLElement>("[data-rigel-text],[data-rigel-richtext],[data-rigel-image],[data-rigel-alt],[data-rigel-href],[data-rigel-section]");
|
|
267
|
+
const element = target.closest<HTMLElement>("[data-rigel-tags],[data-rigel-text],[data-rigel-richtext],[data-rigel-image],[data-rigel-alt],[data-rigel-href],[data-rigel-section]");
|
|
267
268
|
if (element) selectFromElement(element);
|
|
268
269
|
}
|
|
269
270
|
|
|
@@ -314,7 +315,7 @@
|
|
|
314
315
|
const target = eventElement(event.target);
|
|
315
316
|
if (target && (editingBody?.contains(target) || bodyScope?.contains(target))) return;
|
|
316
317
|
if (!target) return;
|
|
317
|
-
const element = target.closest<HTMLElement>("[data-rigel-text],[data-rigel-richtext],[data-rigel-image],[data-rigel-alt],[data-rigel-href],[data-rigel-section]");
|
|
318
|
+
const element = target.closest<HTMLElement>("[data-rigel-tags],[data-rigel-text],[data-rigel-richtext],[data-rigel-image],[data-rigel-alt],[data-rigel-href],[data-rigel-section]");
|
|
318
319
|
if (!element || element === highlighted) return;
|
|
319
320
|
if (hovered && hovered !== element) clearHover();
|
|
320
321
|
hovered = element;
|
|
@@ -324,7 +325,7 @@
|
|
|
324
325
|
function onOut(event: MouseEvent) {
|
|
325
326
|
const target = eventElement(event.target);
|
|
326
327
|
if (!target) return;
|
|
327
|
-
const element = target.closest<HTMLElement>("[data-rigel-text],[data-rigel-richtext],[data-rigel-image],[data-rigel-alt],[data-rigel-href],[data-rigel-section]");
|
|
328
|
+
const element = target.closest<HTMLElement>("[data-rigel-tags],[data-rigel-text],[data-rigel-richtext],[data-rigel-image],[data-rigel-alt],[data-rigel-href],[data-rigel-section]");
|
|
328
329
|
if (element && element === hovered) clearHover();
|
|
329
330
|
}
|
|
330
331
|
|
|
@@ -337,7 +338,7 @@
|
|
|
337
338
|
structureSignature = "";
|
|
338
339
|
const doc = docElement();
|
|
339
340
|
if (!doc) {
|
|
340
|
-
error = "
|
|
341
|
+
error = $t("richtext.pageLoadError");
|
|
341
342
|
return;
|
|
342
343
|
}
|
|
343
344
|
try {
|
|
@@ -348,12 +349,12 @@
|
|
|
348
349
|
doc.addEventListener("mouseout", onOut, true);
|
|
349
350
|
doc.addEventListener("submit", (event) => event.preventDefault(), true);
|
|
350
351
|
doc.defaultView?.addEventListener("scroll", () => closeInline(), { passive: true });
|
|
351
|
-
if (!doc.querySelector(schema ? `[data-rigel-doc="${docId}"]` : "[data-rigel-text],[data-rigel-richtext],[data-rigel-image]")) throw new Error("
|
|
352
|
+
if (!doc.querySelector(schema ? `[data-rigel-doc="${docId}"]` : "[data-rigel-tags],[data-rigel-text],[data-rigel-richtext],[data-rigel-image]")) throw new Error($t("richtext.liveEditingUnavailable"));
|
|
352
353
|
loaded = true;
|
|
353
354
|
bodyAvailable = !schema && !!body && !!doc.querySelector('[data-rigel-richtext="$body"]');
|
|
354
355
|
patch();
|
|
355
356
|
} catch (cause) {
|
|
356
|
-
error = cause instanceof Error ? cause.message : "
|
|
357
|
+
error = cause instanceof Error ? cause.message : $t("richtext.previewUnavailable");
|
|
357
358
|
}
|
|
358
359
|
}
|
|
359
360
|
|
|
@@ -438,14 +439,14 @@
|
|
|
438
439
|
<div class="visual-editor" bind:clientWidth={editorWidth} class:with-panel={panelOpen} class:resizing-properties={resizing} style={`--properties-width: ${panelWidth}px`}>
|
|
439
440
|
<div class="visual-canvas">
|
|
440
441
|
<div class="visual-toolbar">
|
|
441
|
-
<div class="mode-toggle" role="group" aria-label="
|
|
442
|
-
<button type="button" aria-pressed={mode === "browse"} onclick={() => setMode("browse")}>
|
|
443
|
-
<button type="button" aria-pressed={mode === "edit"} onclick={() => setMode("edit")}>
|
|
442
|
+
<div class="mode-toggle" role="group" aria-label={$t("richtext.previewMode")}>
|
|
443
|
+
<button type="button" aria-pressed={mode === "browse"} onclick={() => setMode("browse")}>{$t("richtext.browse")}</button>
|
|
444
|
+
<button type="button" aria-pressed={mode === "edit"} onclick={() => setMode("edit")}>{$t("richtext.edit")}</button>
|
|
444
445
|
</div>
|
|
445
|
-
<span class="visual-hint">{mode === "edit" ? "
|
|
446
|
+
<span class="visual-hint">{mode === "edit" ? $t("richtext.editPreviewHint") : $t("richtext.browsePreviewHint")}</span>
|
|
446
447
|
<div class="visual-actions">
|
|
447
|
-
{#if websitePath}<a class="icon-btn visual-live" href={websitePath} target="_blank" rel="noopener noreferrer" aria-label="
|
|
448
|
-
<button type="button" class="icon-btn" aria-pressed={panelOpen} aria-label="
|
|
448
|
+
{#if websitePath}<a class="icon-btn visual-live" href={websitePath} target="_blank" rel="noopener noreferrer" aria-label={$t("richtext.previewOnWebsite")} data-tooltip={$t("richtext.previewOnWebsite")}><Icon name="external" size={18} /></a>{/if}
|
|
449
|
+
<button type="button" class="icon-btn" aria-pressed={panelOpen} aria-label={$t("richtext.togglePropertiesPanel")} data-tooltip={$t("richtext.togglePropertiesPanel")} onclick={() => (panelOpen = !panelOpen)}>
|
|
449
450
|
<SidebarIcon name="square-half" />
|
|
450
451
|
</button>
|
|
451
452
|
</div>
|
|
@@ -455,16 +456,16 @@
|
|
|
455
456
|
<p class="visual-error" role="status">{error}</p>
|
|
456
457
|
{/if}
|
|
457
458
|
{#if !previewPath}
|
|
458
|
-
<p class="visual-error" role="status">{creating ? "
|
|
459
|
+
<p class="visual-error" role="status">{creating ? $t("richtext.newPagePreviewHint") : $t("richtext.noDocumentPreview")}</p>
|
|
459
460
|
{:else}
|
|
460
|
-
<PreviewFrame bind:frame url={previewPath} {reloadToken} title="
|
|
461
|
+
<PreviewFrame bind:frame url={previewPath} {reloadToken} title={$t("richtext.websitePageEditor")} onload={onLoad} onloading={() => { stopBodyEdit(); inline = null; loaded = false; bodyAvailable = false; error = ""; }} onerror={(message) => { loaded = false; error = message; }} />
|
|
461
462
|
{/if}
|
|
462
463
|
{#if inline}
|
|
463
464
|
<input
|
|
464
465
|
class="inline-editor"
|
|
465
466
|
bind:this={inlineInput}
|
|
466
467
|
type="text"
|
|
467
|
-
aria-label="
|
|
468
|
+
aria-label={$t("richtext.editText")}
|
|
468
469
|
style={`left:${inline.left}px;top:${inline.top}px;width:${inline.width}px;height:${inline.height}px;font-size:${inline.fontSize}`}
|
|
469
470
|
value={inline.value}
|
|
470
471
|
oninput={(event) => updateInline(event.currentTarget.value)}
|
|
@@ -477,7 +478,7 @@
|
|
|
477
478
|
</div>
|
|
478
479
|
</div>
|
|
479
480
|
{#if panelOpen}
|
|
480
|
-
<div class="properties-divider" role="separator" tabindex="0" aria-label="
|
|
481
|
+
<div class="properties-divider" role="separator" tabindex="0" aria-label={$t("richtext.propertiesPanelWidth")} aria-orientation="vertical" aria-valuemin="280" aria-valuemax={panelMax} aria-valuenow={panelWidth} aria-valuetext={$t("richtext.panelPixels", { width: Math.round(panelWidth) })} onpointerdown={startResize} onpointermove={moveResize} onpointerup={endResize} onpointercancel={endResize} onlostpointercapture={endResize} onkeydown={keyResize}></div>
|
|
481
482
|
{/if}
|
|
482
483
|
{#if panelOpen && properties}
|
|
483
484
|
{@render properties(selected, focusToken, inlineBodyEnabled || inlineBodyLoading, focusBody, inlineBodyLoading)}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { get } from "svelte/store";
|
|
2
|
+
import { locale, translate } from "../i18n";
|
|
3
|
+
|
|
4
|
+
export function monthLabels(t = translate): string[] {
|
|
5
|
+
return ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"].map(month => t(`fields.month.${month}`));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const months = monthLabels();
|
|
3
9
|
|
|
4
10
|
export function isoDate(date: Date): string {
|
|
5
11
|
return `${String(date.getUTCFullYear()).padStart(4, "0")}-${String(date.getUTCMonth() + 1).padStart(2, "0")}-${String(date.getUTCDate()).padStart(2, "0")}`;
|
|
@@ -13,9 +19,9 @@ export function parseDate(value: string): Date | null {
|
|
|
13
19
|
return isoDate(date) === value ? date : null;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
|
-
export function formatDate(value: string): string {
|
|
22
|
+
export function formatDate(value: string, language = get(locale)): string {
|
|
17
23
|
const date = parseDate(value);
|
|
18
|
-
return date ?
|
|
24
|
+
return date ? new Intl.DateTimeFormat(language === "en" ? "en-US" : language, { month: "long", day: "numeric", year: "numeric", timeZone: "UTC" }).format(date) : value;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export function todayDate(date = new Date()): string {
|
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import { embedProviderLabel, embedWatchUrl, safeEmbed } from "./embeds";
|
|
2
|
+
import { t, translate } from "../i18n";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
const labels = new WeakMap<HTMLElement, () => string>();
|
|
5
|
+
|
|
6
|
+
function mediaLabel(element: HTMLElement, text: () => string) {
|
|
7
|
+
element.dataset.rigelMediaLabel = "";
|
|
8
|
+
labels.set(element, text);
|
|
9
|
+
element.textContent = text();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function subscribeMediaLabels(root: HTMLElement) {
|
|
13
|
+
return t.subscribe(() => {
|
|
14
|
+
for (const element of root.querySelectorAll<HTMLElement>("[data-rigel-media-label]")) {
|
|
15
|
+
const text = labels.get(element)?.();
|
|
16
|
+
if (text !== undefined && element.textContent !== text) element.textContent = text;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createVideoOverlay(doc: Document, label: () => string, url: string, editable = false): HTMLElement {
|
|
4
22
|
const overlay = doc.createElement("div");
|
|
5
23
|
overlay.contentEditable = "false";
|
|
6
24
|
overlay.style.cssText = "position:absolute;inset:0;display:grid;place-items:center;padding:1rem;background:#0006";
|
|
7
25
|
const card = doc.createElement("div");
|
|
8
26
|
card.style.cssText = "box-sizing:border-box;width:100%;max-width:360px;padding:1rem;background:#18181b;color:white;border:1px solid #52525b;font:14px/1.5 system-ui;text-align:left";
|
|
9
27
|
const heading = doc.createElement("strong");
|
|
10
|
-
heading
|
|
28
|
+
mediaLabel(heading, label);
|
|
11
29
|
const link = doc.createElement("div");
|
|
12
30
|
link.textContent = url;
|
|
13
31
|
link.style.cssText = "margin:0.5rem 0 1rem;overflow-wrap:anywhere;color:#e4e4e7";
|
|
@@ -19,7 +37,7 @@ export function createVideoOverlay(doc: Document, label: string, url: string, ed
|
|
|
19
37
|
event.stopPropagation();
|
|
20
38
|
if (event.key === "Enter" || event.key === " ") { event.preventDefault(); edit.click(); }
|
|
21
39
|
});
|
|
22
|
-
edit
|
|
40
|
+
mediaLabel(edit, () => translate("media.editVideo"));
|
|
23
41
|
edit.hidden = !editable;
|
|
24
42
|
edit.style.cssText = "min-height:44px;padding:0.5rem 0.75rem;border:1px solid #d4d4d8;background:white;color:#18181b;font:inherit;cursor:pointer";
|
|
25
43
|
card.append(heading, link, edit);
|
|
@@ -49,9 +67,9 @@ export function createEmbedPreview(value: unknown, doc: Document, editable = fal
|
|
|
49
67
|
poster.append(image);
|
|
50
68
|
}
|
|
51
69
|
const caption = doc.createElement("figcaption");
|
|
52
|
-
caption.
|
|
70
|
+
mediaLabel(caption, () => translate("media.embedCaption", { provider: embedProviderLabel(ref), title: title || translate("media.video") }));
|
|
53
71
|
caption.style.cssText = "margin-top:0.5rem;font-size:0.875rem;line-height:1.5";
|
|
54
|
-
poster.append(createVideoOverlay(doc,
|
|
72
|
+
poster.append(createVideoOverlay(doc, () => translate("media.providerVideo", { provider: embedProviderLabel(ref) }), embedWatchUrl(ref), editable));
|
|
55
73
|
figure.append(poster, caption);
|
|
56
74
|
return figure;
|
|
57
75
|
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import modules from "virtual:rigel-extensions";
|
|
2
|
+
import { mount, unmount } from "svelte";
|
|
3
|
+
import { $getRoot, $getNodeByKey, type LexicalEditor } from "lexical";
|
|
4
|
+
import { resolveExtensions } from "../../extensions";
|
|
5
|
+
import type { BlockData, ExtensionBlock } from "../../extensions";
|
|
6
|
+
import { ExtensionNode, cloneBlockData } from "../blocks/extension";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
const resolved = resolveExtensions(modules);
|
|
10
|
+
export const extensionRegistry = {
|
|
11
|
+
commands: Object.fromEntries(resolved.commands.map(command => [command.name, command])),
|
|
12
|
+
blocks: Object.fromEntries(resolved.blocks.map(block => [block.name, block])),
|
|
13
|
+
};
|
|
14
|
+
export function extensionBlock(name: string): ExtensionBlock | undefined {
|
|
15
|
+
return Object.hasOwn(extensionRegistry.blocks, name) ? extensionRegistry.blocks[name] : undefined;
|
|
16
|
+
}
|
|
17
|
+
export function reportExtensionError(target: HTMLElement, cause: unknown) {
|
|
18
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
19
|
+
console.error("Rigel extension:", cause);
|
|
20
|
+
target.dispatchEvent(new target.ownerDocument.defaultView!.CustomEvent("rigel-extension-error", { bubbles: true, detail: message }));
|
|
21
|
+
}
|
|
22
|
+
type ExtensionMountHandle = (() => void) & { update: (data: BlockData) => void };
|
|
23
|
+
export function mountExtensionComponent(target: HTMLElement, name: string, raw: unknown, onchange?: (data: BlockData) => void): ExtensionMountHandle {
|
|
24
|
+
const report = (cause: unknown) => reportExtensionError(target, cause);
|
|
25
|
+
const block = extensionBlock(name);
|
|
26
|
+
const fallback = (data: unknown) => { target.textContent = `Unavailable extension: ${block?.label ?? name}\n${JSON.stringify(data, null, 2)}`; };
|
|
27
|
+
const failed = Object.assign(() => {}, { update: fallback });
|
|
28
|
+
target.replaceChildren();
|
|
29
|
+
if (!block) { fallback(raw); return failed; }
|
|
30
|
+
try {
|
|
31
|
+
let data = cloneBlockData(raw);
|
|
32
|
+
block.validate(cloneBlockData(data));
|
|
33
|
+
let disposed = false;
|
|
34
|
+
let component: { update: (data: BlockData) => void } | undefined;
|
|
35
|
+
void import("./ExtensionMount.svelte").then(({ default: ExtensionMount }) => {
|
|
36
|
+
if (disposed || !target.isConnected) return;
|
|
37
|
+
component = mount(ExtensionMount, { target, props: { component: onchange ? block.editor : block.render, data, onchange, report } }) as unknown as { update: (data: BlockData) => void };
|
|
38
|
+
}).catch(cause => { if (!disposed) { fallback(data); report(cause); } });
|
|
39
|
+
return Object.assign(() => { disposed = true; if (component) void unmount(component).catch(report); }, { update: (next: BlockData) => {
|
|
40
|
+
try { const copy = cloneBlockData(next); block.validate(cloneBlockData(copy)); data = copy; component?.update(copy); }
|
|
41
|
+
catch (cause) { report(cause); }
|
|
42
|
+
} });
|
|
43
|
+
} catch (cause) { fallback(raw); report(cause); return failed; }
|
|
44
|
+
}
|
|
45
|
+
export function createExtensionPreview(name: string, data: unknown, doc: Document) {
|
|
46
|
+
const element = doc.createElement("div");
|
|
47
|
+
element.dataset.rigelExtension = name;
|
|
48
|
+
element.dataset.rigelExtensionData = JSON.stringify(cloneBlockData(data));
|
|
49
|
+
element.textContent = `Extension: ${name}`;
|
|
50
|
+
return element;
|
|
51
|
+
}
|
|
52
|
+
const previewMounts = new WeakMap<HTMLElement, ExtensionMountHandle>();
|
|
53
|
+
export function mountExtensionPreviews(root: ParentNode): () => void {
|
|
54
|
+
const doc = root.nodeType === 9 ? root as Document : root.ownerDocument!;
|
|
55
|
+
const frame = doc.defaultView?.frameElement;
|
|
56
|
+
const mounted = new Map<HTMLElement, { stop: ExtensionMountHandle; signature: string }>();
|
|
57
|
+
const markers = () => [...(root.nodeType === 1 && (root as Element).hasAttribute("data-rigel-extension") ? [root as HTMLElement] : []), ...root.querySelectorAll<HTMLElement>("[data-rigel-extension]")];
|
|
58
|
+
const sync = () => {
|
|
59
|
+
if (frame && !frame.isConnected) { cleanup(); return; }
|
|
60
|
+
for (const [element, entry] of mounted) if (!element.isConnected || !root.contains(element) || element.closest('[contenteditable="true"]')) { entry.stop(); mounted.delete(element); previewMounts.delete(element); }
|
|
61
|
+
for (const element of markers()) {
|
|
62
|
+
if (!element.isConnected || element.closest('[contenteditable="true"]')) continue;
|
|
63
|
+
const signature = `${element.dataset.rigelExtension}\n${element.dataset.rigelExtensionData}`;
|
|
64
|
+
const existing = mounted.get(element);
|
|
65
|
+
if (existing?.signature === signature || (!existing && previewMounts.has(element))) continue;
|
|
66
|
+
if (existing) { existing.stop(); mounted.delete(element); previewMounts.delete(element); }
|
|
67
|
+
try {
|
|
68
|
+
const stop = mountExtensionComponent(element, element.dataset.rigelExtension!, JSON.parse(element.dataset.rigelExtensionData ?? "null"));
|
|
69
|
+
mounted.set(element, { stop, signature }); previewMounts.set(element, stop);
|
|
70
|
+
} catch (cause) { element.textContent = "Extension unavailable"; reportExtensionError(element, cause); }
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const observer = new doc.defaultView!.MutationObserver(sync);
|
|
74
|
+
observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ["data-rigel-extension", "data-rigel-extension-data", "contenteditable"] });
|
|
75
|
+
if (frame) observer.observe(frame.ownerDocument, { childList: true, subtree: true });
|
|
76
|
+
const cleanup = () => { observer.disconnect(); for (const [element, entry] of mounted) { entry.stop(); previewMounts.delete(element); } mounted.clear(); doc.defaultView?.removeEventListener("pagehide", cleanup); };
|
|
77
|
+
doc.defaultView?.addEventListener("pagehide", cleanup);
|
|
78
|
+
sync();
|
|
79
|
+
return cleanup;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function registerExtensionNodes(editor: LexicalEditor) {
|
|
83
|
+
const mounted = new Map<string, { element: HTMLElement; name: string; json: string; stop: ExtensionMountHandle }>();
|
|
84
|
+
const doc = editor.getRootElement()!.ownerDocument;
|
|
85
|
+
const frame = doc.defaultView?.frameElement;
|
|
86
|
+
let disposed = false;
|
|
87
|
+
const sync = () => {
|
|
88
|
+
if (disposed) return;
|
|
89
|
+
if (frame && !frame.isConnected) { cleanup(); return; }
|
|
90
|
+
editor.getEditorState().read(() => {
|
|
91
|
+
const nodes = new Map($getRoot().getChildren().filter((node): node is ExtensionNode => node instanceof ExtensionNode).map(node => [node.getKey(), node]));
|
|
92
|
+
for (const [key, entry] of mounted) {
|
|
93
|
+
const node = nodes.get(key), element = editor.getElementByKey(key);
|
|
94
|
+
if (!node || !element?.isConnected || element !== entry.element || node.__name !== entry.name) { entry.stop(); mounted.delete(key); }
|
|
95
|
+
}
|
|
96
|
+
for (const [key, node] of nodes) {
|
|
97
|
+
const element = editor.getElementByKey(key);
|
|
98
|
+
if (!element?.isConnected) continue;
|
|
99
|
+
const json = JSON.stringify(node.__data), existing = mounted.get(key);
|
|
100
|
+
if (existing) { if (existing.json !== json) { existing.json = json; existing.stop.update(node.__data); } continue; }
|
|
101
|
+
const stop = mountExtensionComponent(element, node.__name, node.__data, data => {
|
|
102
|
+
try {
|
|
103
|
+
const copy = cloneBlockData(data), block = extensionBlock(node.__name);
|
|
104
|
+
if (disposed || !element.isConnected || !mounted.has(key) || !editor.isEditable() || !block) throw new Error("Extension editor is no longer active");
|
|
105
|
+
block.validate(cloneBlockData(copy));
|
|
106
|
+
editor.update(() => { const current = $getNodeByKey(key); if (current instanceof ExtensionNode) current.setData(copy); }, { discrete: true, tag: "history-push" });
|
|
107
|
+
} catch (cause) { reportExtensionError(element, cause); }
|
|
108
|
+
});
|
|
109
|
+
mounted.set(key, { element, name: node.__name, json, stop });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
const observer = new doc.defaultView!.MutationObserver(sync);
|
|
114
|
+
observer.observe(doc, { childList: true, subtree: true });
|
|
115
|
+
if (frame) observer.observe(frame.ownerDocument, { childList: true, subtree: true });
|
|
116
|
+
const stopUpdates = editor.registerUpdateListener(sync);
|
|
117
|
+
const cleanup = () => { if (disposed) return; disposed = true; stopUpdates(); observer.disconnect(); for (const entry of mounted.values()) entry.stop(); mounted.clear(); doc.defaultView?.removeEventListener("pagehide", cleanup); };
|
|
118
|
+
doc.defaultView?.addEventListener("pagehide", cleanup);
|
|
119
|
+
sync();
|
|
120
|
+
return cleanup;
|
|
121
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { translate } from "../i18n";
|
|
1
2
|
export interface GitHubAccess {
|
|
2
3
|
user: { login: string; name?: string; avatar_url: string; html_url: string };
|
|
3
4
|
status: "ready" | "installation-required" | "read-only";
|
|
@@ -12,7 +13,7 @@ export function startGitHubLogin(authOrigin: string): Promise<{ accessToken: str
|
|
|
12
13
|
try {
|
|
13
14
|
origin = new URL(authOrigin);
|
|
14
15
|
if (origin.username || origin.password || origin.search || origin.hash || origin.pathname !== "/" || (origin.protocol !== "https:" && !(origin.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(origin.hostname)))) throw new Error();
|
|
15
|
-
} catch { reject(new Error("
|
|
16
|
+
} catch { reject(new Error(translate("connection.invalidAuthOrigin"))); return; }
|
|
16
17
|
const nonce = Array.from(crypto.getRandomValues(new Uint8Array(32)), byte => byte.toString(16).padStart(2, "0")).join("");
|
|
17
18
|
const url = new URL("/authorize", origin);
|
|
18
19
|
url.searchParams.set("origin", window.location.origin);
|
|
@@ -33,27 +34,27 @@ export function startGitHubLogin(authOrigin: string): Promise<{ accessToken: str
|
|
|
33
34
|
if (!popup || event.source !== popup || event.origin !== origin.origin || !data || data.type !== "rigel:auth" || data.provider !== "github" || data.nonce !== nonce) return;
|
|
34
35
|
if (data.error) {
|
|
35
36
|
finish(data.error === "Authorization denied"
|
|
36
|
-
? "
|
|
37
|
+
? translate("connection.authorizationDenied")
|
|
37
38
|
: data.error === "Token exchange failed"
|
|
38
|
-
? "
|
|
39
|
-
: "
|
|
39
|
+
? translate("connection.tokenExchangeFailed")
|
|
40
|
+
: translate("connection.loginFailed"));
|
|
40
41
|
return;
|
|
41
42
|
}
|
|
42
|
-
if (typeof data.access_token !== "string" || !/^ghu_[a-zA-Z0-9_]{1,4092}$/.test(data.access_token) || !Number.isInteger(data.expires_in) || data.expires_in <= 0 || data.expires_in > 86400) { finish("
|
|
43
|
+
if (typeof data.access_token !== "string" || !/^ghu_[a-zA-Z0-9_]{1,4092}$/.test(data.access_token) || !Number.isInteger(data.expires_in) || data.expires_in <= 0 || data.expires_in > 86400) { finish(translate("connection.invalidCredentials")); return; }
|
|
43
44
|
finish(undefined, { accessToken: data.access_token, expiresAt: Date.now() + data.expires_in * 1000 });
|
|
44
45
|
};
|
|
45
46
|
window.addEventListener("message", receive);
|
|
46
47
|
try { popup = window.open(url.href, "rigel-github-" + nonce, "popup,width=600,height=720"); }
|
|
47
|
-
catch { finish("
|
|
48
|
-
if (!popup) { finish("
|
|
49
|
-
timer = setInterval(() => { if (popup?.closed) finish("
|
|
50
|
-
timeout = setTimeout(() => finish("
|
|
48
|
+
catch { finish(translate("connection.loginOpenFailed")); return; }
|
|
49
|
+
if (!popup) { finish(translate("connection.loginBlocked")); return; }
|
|
50
|
+
timer = setInterval(() => { if (popup?.closed) finish(translate("connection.loginCancelled")); }, 250);
|
|
51
|
+
timeout = setTimeout(() => finish(translate("connection.loginTimeout")), 120000);
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
|
|
54
55
|
export async function checkGitHubAccess(token: string, repo: string, appSlug: string): Promise<GitHubAccess> {
|
|
55
|
-
if (typeof token !== "string" || !/^ghu_[a-zA-Z0-9_]{1,4092}$/.test(token)) throw new Error("
|
|
56
|
-
if (typeof repo !== "string" || !/^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\/[a-zA-Z0-9_.-]+$/.test(repo) || [".", ".."].includes(repo.split("/")[1]) || typeof appSlug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(appSlug)) throw new Error("
|
|
56
|
+
if (typeof token !== "string" || !/^ghu_[a-zA-Z0-9_]{1,4092}$/.test(token)) throw new Error(translate("connection.invalidSession"));
|
|
57
|
+
if (typeof repo !== "string" || !/^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\/[a-zA-Z0-9_.-]+$/.test(repo) || [".", ".."].includes(repo.split("/")[1]) || typeof appSlug !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(appSlug)) throw new Error(translate("connection.invalidConfiguration"));
|
|
57
58
|
const get = async (endpoint: string) => {
|
|
58
59
|
let response: Response;
|
|
59
60
|
try {
|
|
@@ -61,30 +62,30 @@ export async function checkGitHubAccess(token: string, repo: string, appSlug: st
|
|
|
61
62
|
method: "GET", redirect: "error", credentials: "omit", cache: "no-store", signal: AbortSignal.timeout(15000),
|
|
62
63
|
headers: { Authorization: `Bearer ${token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28" },
|
|
63
64
|
});
|
|
64
|
-
} catch { throw new Error("
|
|
65
|
+
} catch { throw new Error(translate("connection.unreachable")); }
|
|
65
66
|
if (!response.ok) {
|
|
66
|
-
if (response.status === 401) throw Object.assign(new Error("
|
|
67
|
-
if (response.status === 429 || (response.status === 403 && (response.headers.get("x-ratelimit-remaining") === "0" || response.headers.has("retry-after")))) throw new Error("
|
|
68
|
-
if (response.status === 403) throw new Error("
|
|
69
|
-
if (response.status === 404) throw new Error("
|
|
70
|
-
throw new Error("
|
|
67
|
+
if (response.status === 401) throw Object.assign(new Error(translate("connection.sessionExpired")), { status: 401 });
|
|
68
|
+
if (response.status === 429 || (response.status === 403 && (response.headers.get("x-ratelimit-remaining") === "0" || response.headers.has("retry-after")))) throw new Error(translate("connection.accessRateLimited"));
|
|
69
|
+
if (response.status === 403) throw new Error(translate("connection.accessDenied"));
|
|
70
|
+
if (response.status === 404) throw new Error(translate("connection.repositoryUnavailable"));
|
|
71
|
+
throw new Error(translate("connection.accessCheckFailed"));
|
|
71
72
|
}
|
|
72
73
|
try { return await response.json(); }
|
|
73
|
-
catch { throw new Error("
|
|
74
|
+
catch { throw new Error(translate("connection.invalidAccessResponse")); }
|
|
74
75
|
};
|
|
75
76
|
const list = async <T>(endpoint: string, key: string): Promise<T[]> => {
|
|
76
77
|
const entries: T[] = [];
|
|
77
78
|
for (let page = 1; page <= 100; page++) {
|
|
78
79
|
const data = await get(`${endpoint}?per_page=100&page=${page}`);
|
|
79
|
-
if (!data || !Array.isArray(data[key])) throw new Error("
|
|
80
|
+
if (!data || !Array.isArray(data[key])) throw new Error(translate("connection.invalidAccessResponse"));
|
|
80
81
|
entries.push(...data[key]);
|
|
81
82
|
if (data[key].length < 100 && !(typeof data.total_count === "number" && entries.length < data.total_count)) return entries;
|
|
82
83
|
if (!data[key].length) break;
|
|
83
84
|
}
|
|
84
|
-
throw new Error("
|
|
85
|
+
throw new Error(translate("connection.incompleteAccessListing"));
|
|
85
86
|
};
|
|
86
87
|
const rawUser = await get("/user");
|
|
87
|
-
if (!rawUser || ![rawUser.login, rawUser.avatar_url, rawUser.html_url].every(value => typeof value === "string" && value)) throw new Error("
|
|
88
|
+
if (!rawUser || ![rawUser.login, rawUser.avatar_url, rawUser.html_url].every(value => typeof value === "string" && value)) throw new Error(translate("connection.invalidUser"));
|
|
88
89
|
const name = typeof rawUser.name === "string" ? rawUser.name.trim() : "";
|
|
89
90
|
const user = { login: rawUser.login, ...(name ? { name } : {}), avatar_url: rawUser.avatar_url, html_url: rawUser.html_url };
|
|
90
91
|
const installUrl = `https://github.com/apps/${appSlug}/installations/new`;
|
|
@@ -106,7 +107,7 @@ export async function checkGitHubAccess(token: string, repo: string, appSlug: st
|
|
|
106
107
|
const repositories = await list<NonNullable<GitHubAccess["repository"]>>(`/user/installations/${installation.id}/repositories`, "repositories");
|
|
107
108
|
const repository = repositories.find(repository => repository && typeof repository.full_name === "string" && repository.full_name.toLowerCase() === repo.toLowerCase());
|
|
108
109
|
if (!repository) continue;
|
|
109
|
-
if (!Number.isSafeInteger(repository.id) || repository.id <= 0 || typeof repository.default_branch !== "string" || !repository.default_branch) throw new Error("
|
|
110
|
+
if (!Number.isSafeInteger(repository.id) || repository.id <= 0 || typeof repository.default_branch !== "string" || !repository.default_branch) throw new Error(translate("connection.invalidRepository"));
|
|
110
111
|
const push = repository.permissions?.push === true;
|
|
111
112
|
return {
|
|
112
113
|
user, status: push && installation.permissions?.contents === "write" && !installation.suspended_at ? "ready" : "read-only", installUrl: configureUrl,
|