Fix guide editor issues 4-10
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
'use strict';
|
||||
|
||||
const BLOCK_KIND_ORDER = { text: 0, code: 1, table: 2 };
|
||||
|
||||
function blockText(block) {
|
||||
if (!block || typeof block !== 'object') return '';
|
||||
for (const key of ['code', 'text', 'body', 'value', 'content']) {
|
||||
const value = block[key];
|
||||
if (value != null && value !== '') return String(value);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function orderedBlocks(step) {
|
||||
const blocks = [];
|
||||
for (const tb of step.textBlocks || []) {
|
||||
blocks.push({ kind: 'text', ...tb });
|
||||
}
|
||||
for (const cb of step.codeBlocks || []) {
|
||||
blocks.push({ kind: 'code', ...cb, code: blockText(cb) });
|
||||
}
|
||||
for (const tbl of step.tableBlocks || []) {
|
||||
blocks.push({ kind: 'table', ...tbl });
|
||||
}
|
||||
return blocks.sort((a, b) => (
|
||||
(Number.isFinite(a.order) ? a.order : 0) - (Number.isFinite(b.order) ? b.order : 0)
|
||||
|| (BLOCK_KIND_ORDER[a.kind] - BLOCK_KIND_ORDER[b.kind])
|
||||
|| String(a.id || '').localeCompare(String(b.id || ''))
|
||||
));
|
||||
}
|
||||
|
||||
function nextBlockOrder(step) {
|
||||
return orderedBlocks(step).reduce((max, block) => Math.max(max, Number.isFinite(block.order) ? block.order : 0), 0) + 1;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
BLOCK_KIND_ORDER,
|
||||
blockText,
|
||||
orderedBlocks,
|
||||
nextBlockOrder,
|
||||
};
|
||||
+19
-2
@@ -7,6 +7,7 @@ const { htmlToText, deepClone } = require('./util');
|
||||
const { systemPlaceholders, resolveScopes, expandPlaceholders } = require('./placeholders');
|
||||
const { decodePng } = require('./png');
|
||||
const { renderAnnotations, applyFocusedView } = require('./raster');
|
||||
const { orderedBlocks, blockText } = require('./blocks');
|
||||
|
||||
/**
|
||||
* The Render AST is the single normalized document model every exporter
|
||||
@@ -75,8 +76,24 @@ function buildRenderAst(store, guideId, { globals = {}, now = new Date(), maxSte
|
||||
descriptionHtml: sanitizeHtml(expand(tb.descriptionHtml || '')),
|
||||
descriptionText: htmlToText(expand(tb.descriptionHtml || '')),
|
||||
})),
|
||||
codeBlocks: step.codeBlocks || [],
|
||||
tableBlocks: step.tableBlocks || [],
|
||||
codeBlocks: (step.codeBlocks || []).map((cb) => ({ ...cb, code: blockText(cb) })),
|
||||
tableBlocks: (step.tableBlocks || []).map((tb) => ({
|
||||
...tb,
|
||||
rows: Array.isArray(tb.rows) ? tb.rows.map((row) => [...row]) : [],
|
||||
})),
|
||||
blocks: orderedBlocks(step).map((block) => {
|
||||
if (block.kind === 'text') {
|
||||
return {
|
||||
...block,
|
||||
title: expand(block.title || ''),
|
||||
descriptionHtml: sanitizeHtml(expand(block.descriptionHtml || '')),
|
||||
descriptionText: htmlToText(expand(block.descriptionHtml || '')),
|
||||
};
|
||||
}
|
||||
if (block.kind === 'code') return { ...block };
|
||||
if (block.kind === 'table') return { ...block };
|
||||
return { ...block };
|
||||
}),
|
||||
links: step.links || [],
|
||||
image: null,
|
||||
};
|
||||
|
||||
+34
-5
@@ -1,7 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
const { newId, nowIso, deepClone } = require('./util');
|
||||
const { newId, nowIso } = require('./util');
|
||||
const { sanitizeHtml } = require('./sanitize');
|
||||
const { blockText } = require('./blocks');
|
||||
|
||||
const SCHEMA_VERSION = 1;
|
||||
|
||||
@@ -49,6 +50,12 @@ function createGuide(fields = {}) {
|
||||
}
|
||||
|
||||
function createStep(fields = {}) {
|
||||
let nextOrder = 1;
|
||||
const takeOrder = (block) => {
|
||||
const order = Number.isFinite(block && block.order) ? block.order : nextOrder;
|
||||
nextOrder = Math.max(nextOrder, order + 1);
|
||||
return order;
|
||||
};
|
||||
return {
|
||||
stepId: fields.stepId || newId('step'),
|
||||
parentStepId: fields.parentStepId || null,
|
||||
@@ -69,9 +76,9 @@ function createStep(fields = {}) {
|
||||
image: fields.image || null, // { originalPath, workingPath, size:{width,height} }
|
||||
extraImages: fields.extraImages || [], // multi-image steps
|
||||
annotations: (fields.annotations || []).map(normalizeAnnotation),
|
||||
textBlocks: (fields.textBlocks || []).map(normalizeTextBlock),
|
||||
codeBlocks: fields.codeBlocks || [], // { id, language, code }
|
||||
tableBlocks: fields.tableBlocks || [], // { id, rows: [[cellText,..],..], headerRow }
|
||||
textBlocks: (fields.textBlocks || []).map((tb) => normalizeTextBlock(tb, takeOrder(tb))),
|
||||
codeBlocks: (fields.codeBlocks || []).map((cb) => normalizeCodeBlock(cb, takeOrder(cb))),
|
||||
tableBlocks: (fields.tableBlocks || []).map((tb) => normalizeTableBlock(tb, takeOrder(tb))),
|
||||
links: fields.links || [], // { id, label, targetStepId }
|
||||
};
|
||||
}
|
||||
@@ -94,16 +101,36 @@ function normalizeAnnotation(a) {
|
||||
return ann;
|
||||
}
|
||||
|
||||
function normalizeTextBlock(tb) {
|
||||
function normalizeTextBlock(tb, order = null) {
|
||||
return {
|
||||
id: tb.id || newId('tb'),
|
||||
position: TEXTBLOCK_POSITIONS.includes(tb.position) ? tb.position : 'after-description',
|
||||
level: TEXTBLOCK_LEVELS.includes(tb.level) ? tb.level : 'info',
|
||||
order: Number.isFinite(tb.order) ? tb.order : order,
|
||||
title: tb.title || '',
|
||||
descriptionHtml: sanitizeHtml(tb.descriptionHtml || ''),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeCodeBlock(cb, order = null) {
|
||||
return {
|
||||
id: cb.id || newId('cb'),
|
||||
order: Number.isFinite(cb.order) ? cb.order : order,
|
||||
language: typeof cb.language === 'string' ? cb.language : '',
|
||||
code: blockText(cb),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTableBlock(tb, order = null) {
|
||||
return {
|
||||
id: tb.id || newId('tbl'),
|
||||
order: Number.isFinite(tb.order) ? tb.order : order,
|
||||
rows: Array.isArray(tb.rows)
|
||||
? tb.rows.map((row) => (Array.isArray(row) ? row.map((cell) => String(cell ?? '')) : []))
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function num(v, fallback) {
|
||||
return Number.isFinite(v) ? v : fallback;
|
||||
}
|
||||
@@ -173,6 +200,8 @@ module.exports = {
|
||||
createStep,
|
||||
normalizeAnnotation,
|
||||
normalizeTextBlock,
|
||||
normalizeCodeBlock,
|
||||
normalizeTableBlock,
|
||||
validateGuide,
|
||||
validateStep,
|
||||
normalizeGuide,
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
const path = require('node:path');
|
||||
const { writeJsonSync, readJsonIfExists, htmlToText } = require('./util');
|
||||
const { blockText } = require('./blocks');
|
||||
|
||||
/**
|
||||
* Local full-text search over guide titles, descriptions, step titles/
|
||||
@@ -57,7 +58,7 @@ class SearchIndex {
|
||||
const parts = [
|
||||
htmlToText(step.descriptionHtml),
|
||||
...(step.textBlocks || []).map((tb) => `${tb.title} ${htmlToText(tb.descriptionHtml)}`),
|
||||
...(step.codeBlocks || []).map((cb) => cb.code || ''),
|
||||
...(step.codeBlocks || []).map((cb) => blockText(cb)),
|
||||
...(step.annotations || []).map((a) => a.text || ''),
|
||||
];
|
||||
this.docs[`s:${guide.guideId}:${step.stepId}`] = {
|
||||
|
||||
+3
-4
@@ -221,9 +221,8 @@ class GuideStore {
|
||||
}
|
||||
|
||||
saveStep(guideId, step) {
|
||||
const stored = deepClone(step);
|
||||
const stored = normalizeStep(deepClone(step));
|
||||
stored.descriptionHtml = sanitizeHtml(stored.descriptionHtml);
|
||||
for (const tb of stored.textBlocks || []) tb.descriptionHtml = sanitizeHtml(tb.descriptionHtml);
|
||||
validateStep(stored);
|
||||
writeJsonSync(path.join(this.stepDir(guideId, step.stepId), 'step.json'), stored);
|
||||
const guide = this.getGuide(guideId);
|
||||
@@ -263,8 +262,8 @@ class GuideStore {
|
||||
}
|
||||
|
||||
/** Replace the working image (crop result). The original is never touched. */
|
||||
setWorkingImage(guideId, stepId, pngBuffer, size) {
|
||||
const step = this.getStep(guideId, stepId);
|
||||
setWorkingImage(guideId, stepId, pngBuffer, size, stepPatch = null) {
|
||||
const step = stepPatch ? deepClone(stepPatch) : this.getStep(guideId, stepId);
|
||||
if (!step.image) throw new Error('step has no image');
|
||||
atomicWriteFileSync(path.join(this.stepDir(guideId, stepId), step.image.workingPath), pngBuffer);
|
||||
step.image.size = size;
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ const { writeJsonSync, readJsonSync, atomicWriteFileSync, nowIso } = require('./
|
||||
* defaults, shareable as .sfglt zip files.
|
||||
*/
|
||||
|
||||
const FORMATS = ['json', 'markdown', 'html-simple', 'html-rich', 'pdf', 'gif', 'image-bundle', 'docx', 'pptx'];
|
||||
const FORMATS = ['json', 'markdown', 'html-simple', 'html-rich', 'confluence', 'pdf', 'gif', 'image-bundle', 'docx', 'pptx'];
|
||||
|
||||
class TemplateManager {
|
||||
constructor(templatesDir) {
|
||||
|
||||
Reference in New Issue
Block a user