All checks were successful
CI / Lint, typecheck, test (push) Successful in 2m57s
CD / Promote to Int (push) Successful in 9s
CI / Auth e2e pack (push) Successful in 5m2s
CI / Import/export fidelity gate (push) Successful in 46s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m10s
CD / Deploy to Test (push) Successful in 14s
CD / Smoke tests against Test (push) Successful in 1m12s
Completes M7: exports and the public read view no longer show raw plugin placeholders (ADR 0008/0009). - PluginFallbackRenderer (api): replaces each plugin-block placeholder in content-cache HTML with its best static form — the block's stored SVG snapshot (block data is author-controlled, so it passes the same DOMPurify sanitizer as uploaded SVG files before entering host HTML), else the manifest fallback from the stored snapshot (text, or an image inlined as a data URI so network-isolated renderers work; tombstone-safe for uninstalled plugins), else the literal '[plugin content]' marker. - Office exports (docx/odt): the export markdown is degraded before pandoc — GFM knows neither the dorfteich-plugin fence nor the section fenced div, so blocks become their fallback text and sections plain quoted blocks (shared replacePluginNodesForExport, AST-level so nesting and embedded blocks inside sections survive). - PDF export applies the HTML fallback pass before building the Gotenberg document — resolving the TODO left in #67. - Public read view: the same fallback pass plus the pond's active section-style CSS inlined as a <style> block, so public pages show styled sections and static plugin content without any plugin runtime. - Covered in export.service.db.test (snapshot SVG sanitized — hostile <script> stripped; manifest text; tombstone text; quoted sections and no fence artifacts in the pandoc input) and shared export-fallbacks tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
50 lines
1.9 KiB
TypeScript
50 lines
1.9 KiB
TypeScript
import { Fragment, Node } from 'prosemirror-model';
|
|
|
|
/**
|
|
* Degrades plugin-owned nodes for office exports (issue #79, ADR 0008/0009):
|
|
* pandoc receives GFM, which knows neither the `dorfteich-plugin` fence nor
|
|
* the section fenced div — left alone they would surface as literal fence
|
|
* text in the .docx. So before serializing the export markdown:
|
|
*
|
|
* - a `plugin_block` becomes a paragraph with its fallback text (the caller
|
|
* resolves it from the manifest snapshot; plugins are a server-side
|
|
* registry this package knows nothing about);
|
|
* - a `section` becomes a blockquote of its content — the "plain quoted
|
|
* block" rendition of a styled container in a format with no CSS.
|
|
*/
|
|
export function replacePluginNodesForExport(
|
|
doc: Node,
|
|
fallbackTextFor: (pluginId: string, blockType: string) => string,
|
|
): Node {
|
|
const schema = doc.type.schema;
|
|
|
|
function mapNode(node: Node): Node {
|
|
if (node.type.name === 'plugin_block') {
|
|
const text = fallbackTextFor(
|
|
node.attrs.pluginId as string,
|
|
node.attrs.blockType as string,
|
|
).trim();
|
|
return schema.node('paragraph', null, text === '' ? [] : [schema.text(text)]);
|
|
}
|
|
const children: Node[] = [];
|
|
node.forEach((child) => children.push(mapNode(child)));
|
|
if (node.type.name === 'section') {
|
|
return schema.node('blockquote', null, Fragment.from(children));
|
|
}
|
|
return node.isLeaf ? node : node.copy(Fragment.from(children));
|
|
}
|
|
|
|
return mapNode(doc);
|
|
}
|
|
|
|
/** The plugin ids referenced by `plugin_block` nodes in `doc`, for resolving
|
|
* their fallbacks in one batch before {@link replacePluginNodesForExport}. */
|
|
export function collectPluginBlockIds(doc: Node): string[] {
|
|
const ids = new Set<string>();
|
|
doc.descendants((node) => {
|
|
if (node.type.name === 'plugin_block') ids.add(node.attrs.pluginId as string);
|
|
return true;
|
|
});
|
|
return [...ids];
|
|
}
|