From 5efa38d46a41e915209fe5742fa3f0bb932df9c4 Mon Sep 17 00:00:00 2001 From: Tyler <3deathsaves@gmail.com> Date: Thu, 11 Jun 2026 22:53:27 -0600 Subject: [PATCH 1/2] Add ProseMirror inserts for the system's HTML blocks - Add module/inserts.mjs registering CONFIG.TextEditor.inserts - Group notable, narrative, quest, advice, pull quote, and habitat & treasure under a "D&D Blocks" menu - Wire registration into init and expose on the dnd5e API - Add EDITOR.DND5E.Inserts localization keys --- dnd5e.mjs | 6 +++++ lang/en.json | 14 ++++++++++++ module/inserts.mjs | 56 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+) create mode 100644 module/inserts.mjs diff --git a/dnd5e.mjs b/dnd5e.mjs index 561f8e1618..7312b22eb3 100644 --- a/dnd5e.mjs +++ b/dnd5e.mjs @@ -23,6 +23,7 @@ import * as dice from "./module/dice/_module.mjs"; import * as documents from "./module/documents/_module.mjs"; import * as enrichers from "./module/enrichers.mjs"; import * as Filter from "./module/filter.mjs"; +import * as inserts from "./module/inserts.mjs"; import * as migrations from "./module/migration.mjs"; import { registerModuleData, registerModuleRedirects, setupModulePacks } from "./module/module-registration.mjs"; import { default as registry } from "./module/registry.mjs"; @@ -43,6 +44,7 @@ globalThis.dnd5e = { documents, enrichers, Filter, + inserts, migrations, registry, ui: {}, @@ -229,6 +231,9 @@ Hooks.once("init", function() { // Enrichers enrichers.registerCustomEnrichers(); + // ProseMirror inserts + inserts.registerProseMirrorInserts(); + // Exhaustion handling documents.ActiveEffect5e.registerHUDListeners(); @@ -670,6 +675,7 @@ export { documents, enrichers, Filter, + inserts, migrations, registry, utils, diff --git a/lang/en.json b/lang/en.json index fe302b7c2f..cd997778e6 100644 --- a/lang/en.json +++ b/lang/en.json @@ -7,6 +7,20 @@ } }, +"EDITOR.DND5E": { + "Inserts": { + "Group": "D&D Blocks", + "Notable": "Notable", + "Narrative": "Narrative", + "Quest": "Quest", + "Advice": "Advice", + "Quote": "Pull Quote", + "FloatLeft": "Float Left", + "FloatRight": "Float Right", + "HabitatTreasure": "Habitat & Treasure" + } +}, + "TYPES.Actor.character": "Player Character", "TYPES.Actor.characterPl": "Player Characters", "TYPES.Actor.encounter": "Encounter", diff --git a/module/inserts.mjs b/module/inserts.mjs new file mode 100644 index 0000000000..f963f21d34 --- /dev/null +++ b/module/inserts.mjs @@ -0,0 +1,56 @@ +/** + * Register the system's special HTML blocks as ProseMirror inserts. + */ +export function registerProseMirrorInserts() { + CONFIG.TextEditor.inserts.push({ + action: "dnd5e-blocks", + title: "EDITOR.DND5E.Inserts.Group", + children: [ + { + action: "dnd5e-block-notable", + title: "EDITOR.DND5E.Inserts.Notable", + html: '' + }, + { + action: "dnd5e-block-narrative", + title: "EDITOR.DND5E.Inserts.Narrative", + html: '' + }, + { + action: "dnd5e-block-quest", + title: "EDITOR.DND5E.Inserts.Quest", + html: '
' + + "

Quest

Quest description.

" + }, + { + action: "dnd5e-block-advice", + title: "EDITOR.DND5E.Inserts.Advice", + html: '
' + + "

Advice

Advice content.

" + }, + { + action: "dnd5e-block-quote", + title: "EDITOR.DND5E.Inserts.Quote", + children: [ + { + action: "dnd5e-block-quote-left", + title: "EDITOR.DND5E.Inserts.FloatLeft", + html: '' + }, + { + action: "dnd5e-block-quote-right", + title: "EDITOR.DND5E.Inserts.FloatRight", + html: '' + } + ] + }, + { + action: "dnd5e-block-habitat-treasure", + title: "EDITOR.DND5E.Inserts.HabitatTreasure", + html: '

Habitat: Any; Treasure: None

' + } + ] + }); +} From 73b83e57655b3365b12e364edb0295ea6f9568e4 Mon Sep 17 00:00:00 2001 From: Tyler <3deathsaves@gmail.com> Date: Fri, 12 Jun 2026 00:37:39 -0600 Subject: [PATCH 2/2] Add ProseMirror inserts for the system's enrichers - Add a "D&D Enrichers" group built dynamically from CONFIG.DND5E: checks, saves, skills, damage, awards, attack, item, lookup, and references - Resolve reference rule titles from their compendium pages; alphabetize submenus, keeping abilities in stat order - Register on the ready hook; scroll and title-case the editor dropdown entries - Add EDITOR.DND5E.Inserts enricher localization keys --- dnd5e.mjs | 6 +- lang/en.json | 12 +++- less/v2/apps.less | 7 +++ module/inserts.mjs | 145 ++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 164 insertions(+), 6 deletions(-) diff --git a/dnd5e.mjs b/dnd5e.mjs index 7312b22eb3..7440b65761 100644 --- a/dnd5e.mjs +++ b/dnd5e.mjs @@ -231,9 +231,6 @@ Hooks.once("init", function() { // Enrichers enrichers.registerCustomEnrichers(); - // ProseMirror inserts - inserts.registerProseMirrorInserts(); - // Exhaustion handling documents.ActiveEffect5e.registerHUDListeners(); @@ -553,6 +550,9 @@ Hooks.once("ready", function() { // Adjust sourced items on actors now that compendium UUID redirects have been initialized game.actors.forEach(a => a.sourcedItems._redirectKeys()); + // ProseMirror inserts (after compendia are available so reference rule titles can resolve) + inserts.registerProseMirrorInserts(); + // Register items by type dnd5e.registry.classes.initialize(); dnd5e.registry.subclasses.initialize(); diff --git a/lang/en.json b/lang/en.json index cd997778e6..95f5579f60 100644 --- a/lang/en.json +++ b/lang/en.json @@ -17,7 +17,17 @@ "Quote": "Pull Quote", "FloatLeft": "Float Left", "FloatRight": "Float Right", - "HabitatTreasure": "Habitat & Treasure" + "HabitatTreasure": "Habitat & Treasure", + "EnrichersGroup": "D&D Enrichers", + "Check": "Ability Check", + "Save": "Saving Throw", + "Skill": "Skill Check", + "Damage": "Damage", + "Attack": "Attack Roll", + "Award": "Award", + "Item": "Item", + "Reference": "Reference", + "Lookup": "Lookup" } }, diff --git a/less/v2/apps.less b/less/v2/apps.less index 99ab0337a7..b1fd9ecc34 100644 --- a/less/v2/apps.less +++ b/less/v2/apps.less @@ -2100,6 +2100,13 @@ dialog.dnd5e2.application { ul { box-shadow: 0 3px 6px var(--dnd5e-shadow-45); } li.divider { border-color: #ddd; } + li[data-action^="dnd5e-"] > span { text-transform: capitalize; } + + ul:has(> li[data-action^="dnd5e-"]):not(:has(> li > ul)) { + max-height: 50vh; + overflow: hidden auto; + } + &.theme-dark { --dropdown-background: var(--dnd5e-color-blue-gray-2); --dropdown-border: transparent; diff --git a/module/inserts.mjs b/module/inserts.mjs index f963f21d34..ce0c678392 100644 --- a/module/inserts.mjs +++ b/module/inserts.mjs @@ -1,7 +1,75 @@ /** - * Register the system's special HTML blocks as ProseMirror inserts. + * Build inline enricher inserts for each entry in a config record, titled by the entry's label. + * @param {string} prefix Action id prefix for each generated child. + * @param {Record} record Config record keyed by entry id. + * @param {(key: string) => string} html Builds the enricher markup for a given entry id. + * @returns {object[]} */ -export function registerProseMirrorInserts() { +function buildEnricherInserts(prefix, record, html) { + return Object.entries(record).map(([key, { label }]) => ({ + action: `${prefix}-${key}`, + title: label, + inline: true, + html: html(key) + })); +} + +/* -------------------------------------------- */ + +/** + * Inserts whose children are abilities and should stay in stat order rather than be alphabetized. + * @type {Set} + */ +const STAT_ORDERED = new Set(["dnd5e-enricher-check", "dnd5e-enricher-save", "dnd5e-reference-ability"]); + +/** + * Recursively order insert entries and their submenus alphabetically by localized title. + * @param {object[]} inserts The insert entries to sort in place. + */ +function sortInserts(inserts) { + inserts.sort((a, b) => game.i18n.localize(a.title).localeCompare(game.i18n.localize(b.title))); + for ( const insert of inserts ) { + if ( insert.children && !STAT_ORDERED.has(insert.action) ) sortInserts(insert.children); + } +} + +/* -------------------------------------------- */ + +/** + * Build reference enricher inserts grouped into a submenu per rule type, one leaf per referenceable entry. + * Entries without a resolvable reference are skipped and duplicate references (e.g. `str`/`strength`) are collapsed. + * Rule entries have no configured label, so their referenced document name is resolved for the title. + * @returns {Promise} One insert group per non-empty rule type. + */ +async function buildReferenceInserts() { + const groups = []; + for ( const [type, { label, references }] of Object.entries(CONFIG.DND5E.ruleTypes) ) { + const record = foundry.utils.getProperty(CONFIG.DND5E, references) ?? {}; + const seen = new Set(); + const children = []; + for ( const [key, source] of Object.entries(record) ) { + const uuid = foundry.utils.getType(source) === "Object" ? source.reference : source; + if ( !uuid || seen.has(uuid) ) continue; + seen.add(uuid); + children.push({ + action: `dnd5e-reference-${type}-${key}`, + title: source?.label ?? source?.name ?? (await fromUuid(uuid))?.name ?? key, + inline: true, + html: `&Reference[${type}=${key}]` + }); + } + if ( children.length ) groups.push({ action: `dnd5e-reference-${type}`, title: label, children }); + } + return groups; +} + +/* -------------------------------------------- */ + +/** + * Register the system's special HTML blocks and enrichers as ProseMirror inserts. + * @returns {Promise} + */ +export async function registerProseMirrorInserts() { CONFIG.TextEditor.inserts.push({ action: "dnd5e-blocks", title: "EDITOR.DND5E.Inserts.Group", @@ -53,4 +121,77 @@ export function registerProseMirrorInserts() { } ] }); + + CONFIG.TextEditor.inserts.push({ + action: "dnd5e-enrichers", + title: "EDITOR.DND5E.Inserts.EnrichersGroup", + children: [ + { + action: "dnd5e-enricher-check", + title: "EDITOR.DND5E.Inserts.Check", + children: buildEnricherInserts("dnd5e-enricher-check", CONFIG.DND5E.abilities, key => `[[/check ability=${key}]]`) + }, + { + action: "dnd5e-enricher-save", + title: "EDITOR.DND5E.Inserts.Save", + children: buildEnricherInserts("dnd5e-enricher-save", CONFIG.DND5E.abilities, key => `[[/save ability=${key}]]`) + }, + { + action: "dnd5e-enricher-skill", + title: "EDITOR.DND5E.Inserts.Skill", + children: buildEnricherInserts("dnd5e-enricher-skill", CONFIG.DND5E.skills, key => `[[/check skill=${key}]]`) + }, + { + action: "dnd5e-enricher-damage", + title: "EDITOR.DND5E.Inserts.Damage", + children: buildEnricherInserts("dnd5e-enricher-damage", { + ...CONFIG.DND5E.damageTypes, + ...Object.fromEntries(Object.entries(CONFIG.DND5E.healingTypes) + .map(([key, type]) => [key, { ...type, label: type.labelShort ?? type.label }])) + }, key => `[[/damage type=${key}]]`) + }, + { + action: "dnd5e-enricher-attack", + title: "EDITOR.DND5E.Inserts.Attack", + inline: true, + html: "[[/attack +5]]" + }, + { + action: "dnd5e-enricher-award", + title: "EDITOR.DND5E.Inserts.Award", + children: [ + ...buildEnricherInserts("dnd5e-enricher-award", CONFIG.DND5E.currencies, + key => `[[/award 50${key}]]`), + { + action: "dnd5e-enricher-award-xp", + title: "DND5E.ExperiencePoints.Label", + inline: true, + html: "[[/award 50xp]]" + } + ] + }, + { + action: "dnd5e-enricher-item", + title: "EDITOR.DND5E.Inserts.Item", + inline: true, + html: "[[/item Longsword]]" + }, + { + action: "dnd5e-enricher-reference", + title: "EDITOR.DND5E.Inserts.Reference", + children: await buildReferenceInserts() + }, + { + action: "dnd5e-enricher-lookup", + title: "EDITOR.DND5E.Inserts.Lookup", + inline: true, + html: "[[lookup @name]]" + } + ] + }); + + // Alphabetize every submenu by localized title. + CONFIG.TextEditor.inserts + .filter(insert => insert.action?.startsWith("dnd5e-")) + .forEach(insert => sortInserts(insert.children ?? [])); }