All files / src/pipeline/rfcxml author-tools.js

85.93% Statements 55/64
53.33% Branches 40/75
100% Functions 6/6
85.93% Lines 55/64

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133          2x 2x 2x 2x   2x     8x 8x       2x 2x                   2x 2x 2x 2x     2x 1x       1x 1x 1x 1x     1x       1x       4x 4x 1x 1x 1x 1x 1x             3x 1x   2x 2x 1x 1x             1x                 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x   1x     1x 1x 1x 1x   1x 1x 1x             1x           2x                  
/**
 * Optional RFCXML validation and IETF-format rendering via local xml2rfc or
 * the IETF Author Tools API (https://author-tools.ietf.org/).
 */
 
const fs = require('fs-extra');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const axios = require('axios');
 
const AUTHOR_TOOLS_BASE = 'https://author-tools.ietf.org/api';
 
function xml2rfcAvailable() {
    const result = spawnSync('xml2rfc', ['--version'], { encoding: 'utf8', shell: true });
    return result.status === 0;
}
 
function runXml2rfc(args, cwd) {
    const result = spawnSync('xml2rfc', args, { encoding: 'utf8', shell: true, cwd });
    return {
        ok: result.status === 0,
        status: result.status,
        stdout: result.stdout || '',
        stderr: result.stderr || '',
        error: result.error
    };
}
 
async function validateWithAuthorTools(xml, filename = 'draft.xml') {
    const form = new FormData();
    form.append('file', new Blob([xml], { type: 'application/xml' }), filename);
    const headers = {};
    Iif (process.env.IETF_API_KEY) {
        headers['X-API-KEY'] = process.env.IETF_API_KEY;
    }
    const response = await axios.post(`${AUTHOR_TOOLS_BASE}/validate`, form, { headers });
    return response.data;
}
 
async function renderWithAuthorTools(xml, format, filename = 'draft.xml') {
    const form = new FormData();
    form.append('file', new Blob([xml], { type: 'application/xml' }), filename);
    const headers = {};
    Iif (process.env.IETF_API_KEY) {
        headers['X-API-KEY'] = process.env.IETF_API_KEY;
    }
    const response = await axios.post(`${AUTHOR_TOOLS_BASE}/render/${format}`, form, {
        headers,
        responseType: format === 'pdf' ? 'arraybuffer' : 'json'
    });
    return response.data;
}
 
async function validateRfcXml(xml, options = {}) {
    const filename = options.filename || 'draft.xml';
    if (options.preferLocal !== false && xml2rfcAvailable()) {
        const tmpDir = options.tmpDir || process.cwd();
        const tmpFile = path.join(tmpDir, filename);
        fs.writeFileSync(tmpFile, xml, 'utf8');
        const result = runXml2rfc([tmpFile, '--out', path.join(tmpDir, 'xml2rfc-validate-out.txt')], tmpDir);
        return {
            engine: 'xml2rfc',
            ok: result.ok,
            output: result.stdout,
            errors: result.ok ? [] : [result.stderr || result.stdout || 'xml2rfc failed']
        };
    }
    if (options.allowRemote === false) {
        return { engine: 'none', ok: true, skipped: true, reason: 'xml2rfc not found and remote validation disabled' };
    }
    try {
        const data = await validateWithAuthorTools(xml, filename);
        const errors = data?.errors || data?.error ? [].concat(data.errors || data.error) : [];
        return {
            engine: 'author-tools',
            ok: errors.length === 0,
            output: data,
            errors
        };
    } catch (error) {
        return {
            engine: 'author-tools',
            ok: false,
            errors: [error.message]
        };
    }
}
 
async function renderRfcXml(xml, format, options = {}) {
    const filename = options.filename || 'draft.xml';
    const outputPath = options.outputPath;
    if (options.preferLocal !== false && xml2rfcAvailable()) {
        const tmpDir = options.tmpDir || (outputPath ? path.dirname(outputPath) : process.cwd());
        const tmpFile = path.join(tmpDir, filename);
        fs.writeFileSync(tmpFile, xml, 'utf8');
        const flag = format === 'html' ? '--html' : format === 'pdf' ? '--pdf' : '--text';
        const dest = outputPath || path.join(tmpDir, `draft.${format === 'text' ? 'txt' : format}`);
        const result = runXml2rfc([flag, tmpFile, '-o', dest], tmpDir);
        return { engine: 'xml2rfc', ok: result.ok, outputPath: dest, errors: result.ok ? [] : [result.stderr || result.stdout] };
    }
    Iif (options.allowRemote === false) {
        return { engine: 'none', ok: false, skipped: true, reason: 'xml2rfc not found and remote render disabled' };
    }
    try {
        const data = await renderWithAuthorTools(xml, format === 'text' ? 'text' : format, filename);
        Eif (outputPath && data) {
            Iif (Buffer.isBuffer(data) || data instanceof ArrayBuffer) {
                fs.writeFileSync(outputPath, Buffer.from(data));
            } else if (data.url) {
                const download = await axios.get(data.url, { responseType: 'arraybuffer' });
                fs.writeFileSync(outputPath, Buffer.from(download.data));
            } else Eif (typeof data === 'string') {
                fs.writeFileSync(outputPath, data, 'utf8');
            } else if (data.content) {
                fs.writeFileSync(outputPath, data.content, 'utf8');
            }
        }
        return { engine: 'author-tools', ok: true, outputPath, output: data, errors: [] };
    } catch (error) {
        return { engine: 'author-tools', ok: false, errors: [error.message] };
    }
}
 
module.exports = {
    AUTHOR_TOOLS_BASE,
    xml2rfcAvailable,
    runXml2rfc,
    validateWithAuthorTools,
    renderWithAuthorTools,
    validateRfcXml,
    renderRfcXml
};