From d235ac73f4f50fd17d1a49d9c6ff3f6ea5860354 Mon Sep 17 00:00:00 2001 From: MoritzWeber Date: Fri, 25 Sep 2026 22:44:06 +0200 Subject: [PATCH 1/3] feat(cms): Add automatic prettier formatting before save --- assets/admin/main.js | 1 + assets/admin/preSave.js | 108 ++++++++++++++++++++++++++++++++++++++++ package-lock.json | 16 ++++-- package.json | 6 ++- 4 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 assets/admin/preSave.js diff --git a/assets/admin/main.js b/assets/admin/main.js index 9eafcb913..a0805af9b 100644 --- a/assets/admin/main.js +++ b/assets/admin/main.js @@ -2,5 +2,6 @@ import CMS from "@sveltia/cms"; import "./fields.js"; import "./shortcodes.js"; +import "./preSave.js"; CMS.init(); diff --git a/assets/admin/preSave.js b/assets/admin/preSave.js new file mode 100644 index 000000000..a726d14e9 --- /dev/null +++ b/assets/admin/preSave.js @@ -0,0 +1,108 @@ +import CMS from "@sveltia/cms"; +import { Map as ImmutableMap, List as ImmutableList } from "immutable"; +import * as prettier from "prettier/standalone"; +import * as markdownPlugin from "prettier/plugins/markdown"; + +function isMarkdownFieldKey(key) { + return key === "body" || key === "de" || key === "en" || key === "fr"; +} + +function convertEscapedNewlinesInTables(markdown) { + return markdown + .split("\n") + .map(function (line) { + var trimmed = line.trim(); + if (trimmed.startsWith("|") && trimmed.endsWith("|")) { + return line.replace(/(?:\\n)+/g, "
"); + } + return line; + }) + .join("\n"); +} + +async function formatMarkdown(markdown) { + try { + var formatted = await prettier.format(markdown, { + parser: "markdown", + plugins: [markdownPlugin], + }); + var withBreaks = convertEscapedNewlinesInTables(formatted); + return withBreaks.replace(/\n+$/, ""); + } catch (error) { + console.error("[preSave] prettier.format failed:", error); + return markdown; + } +} + +async function formatMarkdownFieldsInValue(value) { + if (ImmutableMap.isMap(value)) { + var entries = Array.from(value.entries()); + var result = value; + for (var i = 0; i < entries.length; i++) { + var key = entries[i][0]; + var val = entries[i][1]; + if (isMarkdownFieldKey(key) && typeof val === "string" && val.trim()) { + result = result.set(key, await formatMarkdown(val)); + } else { + var formattedVal = await formatMarkdownFieldsInValue(val); + if (formattedVal !== val) { + result = result.set(key, formattedVal); + } + } + } + return result; + } + + if (ImmutableList.isList(value)) { + var items = value.toArray(); + var changed = false; + var newItems = []; + for (var j = 0; j < items.length; j++) { + var newItem = await formatMarkdownFieldsInValue(items[j]); + if (newItem !== items[j]) { + changed = true; + } + newItems.push(newItem); + } + return changed ? ImmutableList(newItems) : value; + } + + return value; +} + +CMS.registerEventListener({ + name: "preSave", + handler: async function (args) { + try { + var entry = args.entry; + var data = entry.get("data"); + var formattedData = await formatMarkdownFieldsInValue(data); + var updatedEntry = + formattedData !== data ? entry.set("data", formattedData) : entry; + + var i18n = updatedEntry.get("i18n"); + if (i18n) { + var locales = Array.from(i18n.keys()); + for (var i = 0; i < locales.length; i++) { + var locale = locales[i]; + var localeData = i18n.getIn([locale, "data"]); + if (localeData) { + var formattedLocaleData = + await formatMarkdownFieldsInValue(localeData); + if (formattedLocaleData !== localeData) { + updatedEntry = updatedEntry.setIn( + ["i18n", locale, "data"], + formattedLocaleData, + ); + } + } + } + } + + return updatedEntry; + } catch (error) { + console.error("[preSave] handler failed:", error); + return args.entry; + } + }, +}); diff --git a/package-lock.json b/package-lock.json index 7edb7d392..0875eb44f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,12 @@ "@material-symbols/svg-700": "^0.47.6", "@panzoom/panzoom": "^4.6.2", "@sveltia/cms": "^0.227.3", - "pagefind": "^1.5.2" + "immutable": "^5.1.9", + "pagefind": "^1.5.2", + "prettier": "^3.9.9", + "react": "19.3.0" }, "devDependencies": { - "prettier": "^3.9.9", "prettier-plugin-go-template": "^0.0.15", "vite": "7.3.6" } @@ -1408,7 +1410,6 @@ "version": "3.9.9", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", - "dev": true, "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" @@ -1436,6 +1437,15 @@ "prettier": "^3.0.0" } }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.63.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz", diff --git a/package.json b/package.json index c5a7108a0..ae2c51235 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,12 @@ "@material-symbols/svg-700": "^0.47.6", "@panzoom/panzoom": "^4.6.2", "@sveltia/cms": "^0.227.3", - "pagefind": "^1.5.2" + "immutable": "^5.1.9", + "pagefind": "^1.5.2", + "prettier": "^3.9.9", + "react": "19.3.0" }, "devDependencies": { - "prettier": "^3.9.9", "prettier-plugin-go-template": "^0.0.15", "vite": "7.3.6" } From 67cccb0b6fcdda291a260f7e9fc6edfbc4d077a1 Mon Sep 17 00:00:00 2001 From: MoritzWeber Date: Sun, 4 Oct 2026 12:49:43 +0200 Subject: [PATCH 2/3] refactor(cms): simplify preSave and reorder content images --- assets/admin/preSave.js | 76 ++++++++--------------------- layouts/admin/section.cmsconfig.yml | 16 +++--- package-lock.json | 13 +---- package.json | 4 +- 4 files changed, 29 insertions(+), 80 deletions(-) diff --git a/assets/admin/preSave.js b/assets/admin/preSave.js index a726d14e9..d41fc2973 100644 --- a/assets/admin/preSave.js +++ b/assets/admin/preSave.js @@ -1,13 +1,19 @@ import CMS from "@sveltia/cms"; -import { Map as ImmutableMap, List as ImmutableList } from "immutable"; import * as prettier from "prettier/standalone"; import * as markdownPlugin from "prettier/plugins/markdown"; -function isMarkdownFieldKey(key) { - return key === "body" || key === "de" || key === "en" || key === "fr"; +function isMapLike(value) { + return ( + value !== null && + typeof value === "object" && + typeof value.get === "function" && + typeof value.set === "function" + ); } function convertEscapedNewlinesInTables(markdown) { + // Workaround for https://github.com/facebook/lexical/issues/9323 + return markdown .split("\n") .map(function (line) { @@ -34,40 +40,18 @@ async function formatMarkdown(markdown) { } } -async function formatMarkdownFieldsInValue(value) { - if (ImmutableMap.isMap(value)) { - var entries = Array.from(value.entries()); - var result = value; - for (var i = 0; i < entries.length; i++) { - var key = entries[i][0]; - var val = entries[i][1]; - if (isMarkdownFieldKey(key) && typeof val === "string" && val.trim()) { - result = result.set(key, await formatMarkdown(val)); - } else { - var formattedVal = await formatMarkdownFieldsInValue(val); - if (formattedVal !== val) { - result = result.set(key, formattedVal); - } - } - } - return result; +async function formatBodyField(data) { + if (!isMapLike(data)) { + return data; } - if (ImmutableList.isList(value)) { - var items = value.toArray(); - var changed = false; - var newItems = []; - for (var j = 0; j < items.length; j++) { - var newItem = await formatMarkdownFieldsInValue(items[j]); - if (newItem !== items[j]) { - changed = true; - } - newItems.push(newItem); - } - return changed ? ImmutableList(newItems) : value; + var body = data.get("body"); + if (typeof body !== "string" || !body.trim()) { + return data; } - return value; + var formattedBody = await formatMarkdown(body); + return formattedBody === body ? data : data.set("body", formattedBody); } CMS.registerEventListener({ @@ -76,30 +60,8 @@ CMS.registerEventListener({ try { var entry = args.entry; var data = entry.get("data"); - var formattedData = await formatMarkdownFieldsInValue(data); - var updatedEntry = - formattedData !== data ? entry.set("data", formattedData) : entry; - - var i18n = updatedEntry.get("i18n"); - if (i18n) { - var locales = Array.from(i18n.keys()); - for (var i = 0; i < locales.length; i++) { - var locale = locales[i]; - var localeData = i18n.getIn([locale, "data"]); - if (localeData) { - var formattedLocaleData = - await formatMarkdownFieldsInValue(localeData); - if (formattedLocaleData !== localeData) { - updatedEntry = updatedEntry.setIn( - ["i18n", locale, "data"], - formattedLocaleData, - ); - } - } - } - } - - return updatedEntry; + var formattedData = await formatBodyField(data); + return formattedData !== data ? entry.set("data", formattedData) : entry; } catch (error) { console.error("[preSave] handler failed:", error); return args.entry; diff --git a/layouts/admin/section.cmsconfig.yml b/layouts/admin/section.cmsconfig.yml index ca9c06201..b32b2e01a 100644 --- a/layouts/admin/section.cmsconfig.yml +++ b/layouts/admin/section.cmsconfig.yml @@ -139,14 +139,6 @@ collections: widget: "string" required: false i18n: true - - label: "Content Images" - name: "content_images" - widget: "image" - multiple: true - required: false - i18n: duplicate - hint: "Images shown for this page. Prefer .webp images." - accept: "image/webp" - label: "Countries" name: "country" widget: "relation" @@ -169,6 +161,14 @@ collections: required: false i18n: duplicate hint: "List of operators this news article is about." + - label: "Content Images" + name: "content_images" + widget: "image" + multiple: true + required: false + i18n: duplicate + hint: "Images shown for this page. Prefer .webp images." + accept: "image/webp" - label: "Body" name: "body" widget: "markdown" diff --git a/package-lock.json b/package-lock.json index 0875eb44f..37b96ff61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,8 @@ "@material-symbols/svg-700": "^0.47.6", "@panzoom/panzoom": "^4.6.2", "@sveltia/cms": "^0.227.3", - "immutable": "^5.1.9", "pagefind": "^1.5.2", - "prettier": "^3.9.9", - "react": "19.3.0" + "prettier": "^3.9.9" }, "devDependencies": { "prettier-plugin-go-template": "^0.0.15", @@ -1437,15 +1435,6 @@ "prettier": "^3.0.0" } }, - "node_modules/react": { - "version": "19.3.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", - "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/rollup": { "version": "4.63.5", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.63.5.tgz", diff --git a/package.json b/package.json index ae2c51235..620524b34 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,8 @@ "@material-symbols/svg-700": "^0.47.6", "@panzoom/panzoom": "^4.6.2", "@sveltia/cms": "^0.227.3", - "immutable": "^5.1.9", "pagefind": "^1.5.2", - "prettier": "^3.9.9", - "react": "19.3.0" + "prettier": "^3.9.9" }, "devDependencies": { "prettier-plugin-go-template": "^0.0.15", From ea201a2491aef97e72cb03632a98a6fb9c9d17f0 Mon Sep 17 00:00:00 2001 From: MoritzWeber Date: Sun, 4 Oct 2026 18:55:18 +0200 Subject: [PATCH 3/3] feat(cms): add footnote reference component and editor icons --- assets/admin/shortcodes.js | 48 ++++++++++++++++++++++++++++- layouts/admin/section.cmsconfig.yml | 13 ++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/assets/admin/shortcodes.js b/assets/admin/shortcodes.js index 2fff5e5d1..2a09b42c4 100644 --- a/assets/admin/shortcodes.js +++ b/assets/admin/shortcodes.js @@ -181,6 +181,7 @@ var highlightFields = [ CMS.registerEditorComponent({ id: "highlight", label: "Highlight", + icon: "stylus_highlighter", fields: highlightFields, pattern: shortcodePattern("highlight", "%"), fromBlock: function (match) { @@ -199,6 +200,7 @@ CMS.registerEditorComponent({ CMS.registerEditorComponent({ id: "highlight-raw", label: "Highlight", + icon: "stylus_highlighter", fields: highlightFields, pattern: shortcodePattern("highlight", "<"), fromBlock: function (match) { @@ -251,6 +253,7 @@ var expanderFields = [ CMS.registerEditorComponent({ id: "expander", label: "Expander", + icon: "arrow_drop_down_circle", fields: expanderFields, pattern: shortcodePattern("expander"), fromBlock: function (match) { @@ -324,6 +327,7 @@ var fipValidityFields = [ CMS.registerEditorComponent({ id: "fip-validity", label: "FIP Validity Badge", + icon: "verified", fields: fipValidityFields, pattern: selfClosingPattern("fip-validity"), fromBlock: function (match) { @@ -443,6 +447,7 @@ var trainCategoryFields = [ CMS.registerEditorComponent({ id: "train-category", label: "Train Category", + icon: "train", fields: trainCategoryFields, pattern: shortcodePattern("train-category"), fromBlock: function (match) { @@ -591,6 +596,7 @@ var bookingFields = [ CMS.registerEditorComponent({ id: "booking", label: "Booking", + icon: "confirmation_number", fields: bookingFields, pattern: dualFormPattern("booking"), fromBlock: function (match) { @@ -653,6 +659,7 @@ var bookingSectionFields = [ CMS.registerEditorComponent({ id: "booking-section", label: "Booking Section", + icon: "transit_ticket", fields: bookingSectionFields, pattern: shortcodePattern("booking-section"), fromBlock: function (match) { @@ -690,6 +697,7 @@ var buttonFields = [ CMS.registerEditorComponent({ id: "button", label: "Button", + icon: "highlight_mouse_cursor", fields: buttonFields, pattern: selfClosingPattern("button"), fromBlock: function (match) { @@ -743,6 +751,7 @@ var identifyOperatorFields = [ CMS.registerEditorComponent({ id: "identify-operator", label: "Identify Operator", + icon: "search", fields: identifyOperatorFields, pattern: dualFormPattern("identify-operator"), fromBlock: function (match) { @@ -780,6 +789,7 @@ var satelliteFields = [ CMS.registerEditorComponent({ id: "satellite", label: "Satellite Notice", + icon: "satellite_alt", fields: satelliteFields, pattern: dualFormPattern("satellite"), fromBlock: function (match) { @@ -819,6 +829,7 @@ var dialogFields = [ CMS.registerEditorComponent({ id: "dialog", label: "Dialog", + icon: "open_in_browser", fields: dialogFields, pattern: shortcodePattern("dialog"), fromBlock: function (match) { @@ -886,6 +897,7 @@ var floatImageFields = [ CMS.registerEditorComponent({ id: "float-image", label: "Float Image", + icon: "format_image_left", fields: floatImageFields, pattern: shortcodePattern("float-image"), fromBlock: function (match) { @@ -912,6 +924,7 @@ CMS.registerEditorComponent({ CMS.registerEditorComponent({ id: "wip", label: "Work in Progress", + icon: "construction", fields: [], pattern: selfClosingPattern("wip"), fromBlock: function () { @@ -955,6 +968,34 @@ CMS.registerEditorComponent({ }, }); +CMS.registerEditorComponent({ + id: "footnote-reference", + label: "Footnote Reference", + icon: "asterisk", + mode: "dialog", + summary: "Footnote [^{{id}}]", + fields: [ + { + name: "id", + label: "Number", + widget: "cms-number-with-usage", + template: + "You need to add the footnote with number {value} at the bottom of the page if it doesn't exist yet.", + required: true, + }, + ], + pattern: /\[\^([^\]]+)\](?!:)/, + fromBlock: function (match) { + return { id: String(match[1] || "") }; + }, + toBlock: function (data) { + return "[^" + (data.id || "") + "]"; + }, + toPreview: function () { + return ""; + }, +}); + var updateFields = [ { name: "date", @@ -975,6 +1016,7 @@ var updateFields = [ CMS.registerEditorComponent({ id: "update", label: "Update", + icon: "update", fields: updateFields, pattern: shortcodePattern("update"), fromBlock: function (match) { @@ -1009,6 +1051,7 @@ var columnFields = [ CMS.registerEditorComponent({ id: "column", label: "Column", + icon: "vertical_split", fields: columnFields, pattern: shortcodePattern("column"), fromBlock: function (match) { @@ -1037,6 +1080,7 @@ var columnsFields = [ CMS.registerEditorComponent({ id: "columns", label: "Columns", + icon: "view_column", fields: columnsFields, pattern: shortcodePattern("columns"), fromBlock: function (match) { @@ -1086,7 +1130,7 @@ var footnoteFields = [ label: "Number", widget: "cms-number-with-usage", template: - "You can use this footnote by writing out [^{value}] in the text editor.", + "You can use the footnote by adding footnote reference with number {value} in the text editor.", param: { required: true }, }, { @@ -1106,6 +1150,7 @@ var footnoteFields = [ CMS.registerEditorComponent({ id: "footnote", label: "Footnote", + icon: "inbox_text_asterisk", fields: footnoteFields, pattern: /^\[\^([^\]]+)\]: \[([^\]]*)\]\(([^)]*)\)$/, fromBlock: function (match) { @@ -1136,6 +1181,7 @@ CMS.registerEditorComponent({ CMS.registerEditorComponent({ id: "fip-validity-table", label: "FIP Validity Table", + icon: "table", fields: fipValidityTableFields, pattern: selfClosingPattern("fip-validity-table"), fromBlock: function (match) { diff --git a/layouts/admin/section.cmsconfig.yml b/layouts/admin/section.cmsconfig.yml index b32b2e01a..4c939383f 100644 --- a/layouts/admin/section.cmsconfig.yml +++ b/layouts/admin/section.cmsconfig.yml @@ -182,6 +182,7 @@ collections: - "float-image" - "wip" - "icon" + - "footnote-reference" - "update" - "columns" - "footnote" @@ -238,6 +239,7 @@ collections: - "float-image" - "wip" - "icon" + - "footnote-reference" - "identify-operator" - "columns" - "footnote" @@ -301,6 +303,7 @@ collections: - "float-image" - "wip" - "icon" + - "footnote-reference" - "columns" - "footnote" @@ -372,6 +375,7 @@ collections: - "float-image" - "wip" - "icon" + - "footnote-reference" - "columns" - "footnote" @@ -416,18 +420,21 @@ collections: widget: "markdown" editor_components: - "icon" + - "footnote-reference" minimal: true - label: "English" name: "en" widget: "markdown" editor_components: - "icon" + - "footnote-reference" minimal: true - label: "Français" name: "fr" widget: "markdown" editor_components: - "icon" + - "footnote-reference" minimal: true - label: "Operators" name: "operators" @@ -697,6 +704,7 @@ collections: - "float-image" - "wip" - "icon" + - "footnote-reference" - "columns" - "footnote" @@ -729,6 +737,7 @@ collections: - "float-image" - "wip" - "icon" + - "footnote-reference" - "columns" - "footnote" @@ -767,6 +776,7 @@ collections: - "float-image" - "wip" - "icon" + - "footnote-reference" - "columns" - "footnote" @@ -805,6 +815,7 @@ collections: - "float-image" - "wip" - "icon" + - "footnote-reference" - "columns" - "footnote" @@ -841,6 +852,7 @@ collections: - "float-image" - "wip" - "icon" + - "footnote-reference" - "columns" - "footnote" @@ -882,5 +894,6 @@ collections: - "float-image" - "wip" - "icon" + - "footnote-reference" - "columns" - "footnote"