2a602d7477
- Folder-based GuideStore with atomic writes, trash/restore, duplicate, substep reparenting, folders/favorites, working-image crop/reset - Allowlist HTML sanitizer applied on every store write - Placeholder scopes (guide > global > system) and collection - Persisted app settings with deep default merge - 16 workflow tests exercising real on-disk round-trips Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
80 lines
2.4 KiB
JavaScript
80 lines
2.4 KiB
JavaScript
'use strict';
|
|
|
|
const { htmlToText } = require('./util');
|
|
|
|
/**
|
|
* Placeholders are [[Name]] tokens usable in titles, descriptions, text
|
|
* blocks, and export cover pages. Resolution precedence (highest wins):
|
|
* guide placeholders > global placeholders > system placeholders.
|
|
* Unknown tokens are left untouched so typos are visible in output.
|
|
*/
|
|
|
|
const TOKEN_RE = /\[\[([A-Za-z0-9_ .-]+)\]\]/g;
|
|
|
|
function systemPlaceholders(guide, { now = new Date(), stepCount = null } = {}) {
|
|
const pad = (n) => String(n).padStart(2, '0');
|
|
const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
|
|
return {
|
|
Guide_Title: guide ? guide.title : '',
|
|
Guide_Description: guide ? htmlToText(guide.descriptionHtml) : '',
|
|
Date: date,
|
|
Time: time,
|
|
DateTime: `${date} ${time}`,
|
|
Year: String(now.getFullYear()),
|
|
Step_Count: stepCount == null ? '' : String(stepCount),
|
|
App_Name: 'StepForge',
|
|
};
|
|
}
|
|
|
|
/** Build the effective name->value map for a guide. */
|
|
function resolveScopes({ guide = null, globals = {}, system = {} } = {}) {
|
|
return { ...system, ...globals, ...(guide && guide.placeholders ? guide.placeholders : {}) };
|
|
}
|
|
|
|
function expandPlaceholders(text, values) {
|
|
if (!text) return text == null ? '' : text;
|
|
return String(text).replace(TOKEN_RE, (whole, name) => {
|
|
const key = name.trim();
|
|
return Object.prototype.hasOwnProperty.call(values, key) && values[key] != null
|
|
? String(values[key])
|
|
: whole;
|
|
});
|
|
}
|
|
|
|
/** List distinct placeholder names used in a string. */
|
|
function listPlaceholders(text) {
|
|
const names = new Set();
|
|
if (text) {
|
|
for (const m of String(text).matchAll(TOKEN_RE)) names.add(m[1].trim());
|
|
}
|
|
return [...names];
|
|
}
|
|
|
|
/** Collect every placeholder name used anywhere in a guide + its steps. */
|
|
function collectGuidePlaceholders(guide, steps) {
|
|
const names = new Set();
|
|
const add = (text) => listPlaceholders(text).forEach((n) => names.add(n));
|
|
add(guide.title);
|
|
add(guide.descriptionHtml);
|
|
for (const step of steps) {
|
|
add(step.title);
|
|
add(step.descriptionHtml);
|
|
for (const tb of step.textBlocks || []) {
|
|
add(tb.title);
|
|
add(tb.descriptionHtml);
|
|
}
|
|
for (const ann of step.annotations || []) add(ann.text);
|
|
}
|
|
return [...names].sort();
|
|
}
|
|
|
|
module.exports = {
|
|
TOKEN_RE,
|
|
systemPlaceholders,
|
|
resolveScopes,
|
|
expandPlaceholders,
|
|
listPlaceholders,
|
|
collectGuidePlaceholders,
|
|
};
|