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..d41fc2973 --- /dev/null +++ b/assets/admin/preSave.js @@ -0,0 +1,70 @@ +import CMS from "@sveltia/cms"; +import * as prettier from "prettier/standalone"; +import * as markdownPlugin from "prettier/plugins/markdown"; + +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) { + 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 formatBodyField(data) { + if (!isMapLike(data)) { + return data; + } + + var body = data.get("body"); + if (typeof body !== "string" || !body.trim()) { + return data; + } + + var formattedBody = await formatMarkdown(body); + return formattedBody === body ? data : data.set("body", formattedBody); +} + +CMS.registerEventListener({ + name: "preSave", + handler: async function (args) { + try { + var entry = args.entry; + var data = entry.get("data"); + 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/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 ca9c06201..4c939383f 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" @@ -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" diff --git a/package-lock.json b/package-lock.json index 7edb7d392..37b96ff61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,10 +14,10 @@ "@material-symbols/svg-700": "^0.47.6", "@panzoom/panzoom": "^4.6.2", "@sveltia/cms": "^0.227.3", - "pagefind": "^1.5.2" + "pagefind": "^1.5.2", + "prettier": "^3.9.9" }, "devDependencies": { - "prettier": "^3.9.9", "prettier-plugin-go-template": "^0.0.15", "vite": "7.3.6" } @@ -1408,7 +1408,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" diff --git a/package.json b/package.json index c5a7108a0..620524b34 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,10 @@ "@material-symbols/svg-700": "^0.47.6", "@panzoom/panzoom": "^4.6.2", "@sveltia/cms": "^0.227.3", - "pagefind": "^1.5.2" + "pagefind": "^1.5.2", + "prettier": "^3.9.9" }, "devDependencies": { - "prettier": "^3.9.9", "prettier-plugin-go-template": "^0.0.15", "vite": "7.3.6" }