Step 1 — Knowledge base: perfect all reference/*.md with type, range, default, unit - text.md: add DataOrPathProps, path/data, bullet deprecated params - chart.md: add deprecated section, fix 19 params with missing elements - slide.md: add bkgd→background deprecated mapping - output.md: add masterSlide, presLayout Step 2 — Schema: align schema/presentation.schema.json with KB - Add catAxisItem and valAxisItem definitions with full sub-property constraints - Fix 7 enum constraints (barDir, displayBlanksAs, bar3DShape, etc.) - Add 4 missing fields (verbose, autoPageCharWeight, autoPageLineWeight, notes, masterSlide, presLayout) Step 3 — Translation engine: transform renderers from passthrough to validation - index.js: add normalizePosition, normalizeColor, validateEnum shared utilities - All renderers: position validation, color hex normalization, enum validation - Image: graceful failure handling (catch load errors, warn, continue) - Chart: chartType enum validation, data parity check - Table: colspan/rowspan integer validation, border array check - Full verification: basic.json and full.json generate valid PPTX Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
130 lines
4.7 KiB
JavaScript
130 lines
4.7 KiB
JavaScript
const pptxgen = require('pptxgenjs');
|
|
const fs = require('fs');
|
|
|
|
function normalizePosition(options) {
|
|
if (!options || typeof options !== 'object') return options;
|
|
['x', 'y', 'w', 'h'].forEach(function(key) {
|
|
if (options[key] === undefined) return;
|
|
var val = options[key];
|
|
if (typeof val === 'number' && !isNaN(val)) return;
|
|
if (typeof val === 'string' && /^\d+(\.\d+)?%$/.test(val)) return;
|
|
console.warn('Invalid position value for "' + key + '": got ' + JSON.stringify(val) + ', expected a number or percentage string (e.g. "50%")');
|
|
delete options[key];
|
|
});
|
|
if ((options.x !== undefined || options.y !== undefined) && options.w === undefined) options.w = 4;
|
|
if ((options.x !== undefined || options.y !== undefined) && options.h === undefined) options.h = 3;
|
|
return options;
|
|
}
|
|
|
|
function normalizeColor(color) {
|
|
if (color === undefined || color === null) return color;
|
|
if (typeof color !== 'string') {
|
|
console.warn('Invalid color value: expected a string, got ' + typeof color);
|
|
return undefined;
|
|
}
|
|
var themeColors = ['tx1', 'tx2', 'bg1', 'bg2', 'accent1', 'accent2', 'accent3', 'accent4', 'accent5', 'accent6'];
|
|
for (var i = 0; i < themeColors.length; i++) {
|
|
if (color === themeColors[i]) return color;
|
|
}
|
|
var clean = color.replace(/^#/, '');
|
|
if (/^[0-9A-Fa-f]{6}$/.test(clean)) return clean;
|
|
console.warn('Invalid color value: "' + color + '" is not a valid color format (expected 6-digit hex or theme color)');
|
|
return undefined;
|
|
}
|
|
|
|
function normalizeColorInOptions(options, key) {
|
|
if (!options || options[key] === undefined) return;
|
|
var nc = normalizeColor(options[key]);
|
|
if (nc === undefined) delete options[key];
|
|
else options[key] = nc;
|
|
}
|
|
|
|
function validateEnum(value, validValues, name) {
|
|
if (value === undefined || value === null) return value;
|
|
if (validValues.indexOf(value) === -1) {
|
|
console.warn('Invalid value for ' + name + ': got "' + value + '", expected one of [' + validValues.join(', ') + ']');
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function withDefaults(options, defaults) {
|
|
if (!options || typeof options !== 'object') return options;
|
|
for (var key in defaults) {
|
|
if (defaults.hasOwnProperty(key) && options[key] === undefined) {
|
|
options[key] = defaults[key];
|
|
}
|
|
}
|
|
return options;
|
|
}
|
|
|
|
module.exports.normalizePosition = normalizePosition;
|
|
module.exports.normalizeColor = normalizeColor;
|
|
module.exports.normalizeColorInOptions = normalizeColorInOptions;
|
|
module.exports.validateEnum = validateEnum;
|
|
module.exports.withDefaults = withDefaults;
|
|
|
|
const { renderSlide } = require('./slide-renderer');
|
|
|
|
function render(pptxJson) {
|
|
const pptx = new pptxgen();
|
|
const data = typeof pptxJson === 'string' ? JSON.parse(pptxJson) : pptxJson;
|
|
const pres = data.presentation || {};
|
|
const slides = data.slides || [];
|
|
|
|
if (pres.layout) pptx.layout = pres.layout;
|
|
if (pres.author) pptx.author = pres.author;
|
|
if (pres.title) pptx.title = pres.title;
|
|
if (pres.subject) pptx.subject = pres.subject;
|
|
if (pres.company) pptx.company = pres.company;
|
|
if (pres.revision) pptx.revision = pres.revision;
|
|
if (pres.rtlMode !== undefined) pptx.rtlMode = pres.rtlMode;
|
|
if (pres.theme) pptx.theme = pres.theme;
|
|
|
|
slides.forEach(slideData => renderSlide(pptx, slideData));
|
|
|
|
return pptx;
|
|
}
|
|
|
|
async function renderFile(inputPath, outputPath) {
|
|
const json = fs.readFileSync(inputPath, 'utf8');
|
|
const pptx = render(json);
|
|
|
|
// pptxgenjs v4 uses https.get() to download URL-based images but doesn't
|
|
// handle connection errors on the request object (only on the response
|
|
// stream). A failed TLS handshake emits an unhandled 'error' event on the
|
|
// request that crashes Node.js, and the internal promise never resolves.
|
|
// Patch to pipe request errors into a fake response that ends cleanly so
|
|
// writeFile completes with an empty placeholder image.
|
|
var https = require('https');
|
|
var _origGet = https.get;
|
|
var EventEmitter = require('events').EventEmitter;
|
|
https.get = function() {
|
|
var args = arguments;
|
|
var cb = typeof args[args.length - 1] === 'function' ? args[args.length - 1] : null;
|
|
var urlStr = typeof args[0] === 'string' ? args[0] : '';
|
|
var req = _origGet.apply(this, args);
|
|
req.on('error', function(err) {
|
|
console.warn('Failed to load image: ' + (urlStr || err.hostname || err.host || 'unknown'));
|
|
if (cb) {
|
|
var fakeRes = new EventEmitter();
|
|
fakeRes.setEncoding = function() {};
|
|
cb(fakeRes);
|
|
process.nextTick(function() {
|
|
fakeRes.emit('data', '');
|
|
fakeRes.emit('end');
|
|
});
|
|
}
|
|
});
|
|
return req;
|
|
};
|
|
|
|
try {
|
|
return await pptx.writeFile({ fileName: outputPath });
|
|
} finally {
|
|
https.get = _origGet;
|
|
}
|
|
}
|
|
|
|
module.exports.render = render;
|
|
module.exports.renderFile = renderFile;
|