Restructure exports to avoid duplication of DOM-related code

This commit is contained in:
David Bruant 2025-04-17 16:10:10 +02:00
parent 30cfe0921e
commit df9abcfb56
17 changed files with 174 additions and 260 deletions

29
exports.js Normal file
View File

@ -0,0 +1,29 @@
//@ts-check
export {default as fillOdtTemplate} from './scripts/odf/fillOdtTemplate.js'
export {getOdtTextContent} from './scripts/odf/odt/getOdtTextContent.js'
export { createOdsFile } from './scripts/createOdsFile.js'
export {
getODSTableRawContent,
// table-level exports
tableWithoutEmptyRows,
tableRawContentToValues,
tableRawContentToStrings,
tableRawContentToObjects,
// sheet-level exports
sheetRawContentToObjects,
sheetRawContentToStrings,
// row-level exports
rowRawContentToStrings,
isRowNotEmpty,
// cell-level exports
cellRawContentToStrings,
convertCellValue
} from './scripts/shared.js'

View File

@ -2,8 +2,13 @@
"name": "@odfjs/odfjs", "name": "@odfjs/odfjs",
"version": "0.14.0", "version": "0.14.0",
"type": "module", "type": "module",
"main": "./scripts/node.js", "exports": "./scripts/exports.js",
"browser": "./scripts/browser.js", "imports": {
"#DOM": {
"node": "./scripts/DOM/node.js",
"browser": "./scripts/DOM/browser.js"
}
},
"scripts": { "scripts": {
"build": "rollup -c", "build": "rollup -c",
"dev": "npm-run-all --parallel dev:* start", "dev": "npm-run-all --parallel dev:* start",

8
scripts/DOM/browser.js Normal file
View File

@ -0,0 +1,8 @@
console.info('DOM implementation in browser')
/** @type { typeof DOMImplementation.prototype.createDocument } */
export function createDocument(...args){
// @ts-ignore
return document.implementation.createDocument(...args)
}

17
scripts/DOM/node.js Normal file
View File

@ -0,0 +1,17 @@
import { DOMImplementation } from "@xmldom/xmldom"
console.info('DOM implementation in Node.js based on xmldom')
const implementation = new DOMImplementation()
/** @type { typeof DOMImplementation.prototype.createDocument } */
export function createDocument(...args){
// @ts-ignore
return implementation.createDocument(...args)
}
export {
DOMParser,
XMLSerializer,
Node
} from "@xmldom/xmldom"

View File

@ -1,8 +1,28 @@
import {DOMParser, XMLSerializer} from '#DOM'
/* /*
Since we're using xmldom in Node.js context, the entire DOM API is not implemented Since we're using xmldom in Node.js context, the entire DOM API is not implemented
Functions here are helpers whild xmldom becomes more complete Functions here are helpers whild xmldom becomes more complete
*/ */
/**
*
* @param {string} str
* @returns {Document}
*/
export function parseXML(str){
return (new DOMParser()).parseFromString(str, 'application/xml');
}
const serializer = new XMLSerializer()
/** @type { typeof XMLSerializer.prototype.serializeToString } */
export function serializeToString(node){
return serializer.serializeToString(node)
}
/** /**
* Traverses a DOM tree starting from the given element and applies the visit function * Traverses a DOM tree starting from the given element and applies the visit function
* to each Element node encountered in tree order (depth-first). * to each Element node encountered in tree order (depth-first).
@ -21,3 +41,10 @@ export function traverse(node, visit) {
visit(node); visit(node);
} }
export {
DOMParser,
XMLSerializer,
createDocument,
Node
} from '#DOM'

View File

@ -1,78 +0,0 @@
//@ts-check
import { _getODSTableRawContent } from './shared.js'
import {_createOdsFile} from './createOdsFile.js'
import _fillOdtTemplate from './odf/fillOdtTemplate.js'
/** @import {SheetCellRawContent, SheetName, SheetRawContent} from './types.js' */
/** @import {ODTFile} from './odf/fillOdtTemplate.js' */
function parseXML(str){
return (new DOMParser()).parseFromString(str, 'application/xml');
}
/**
* @param {ArrayBuffer} odsArrBuff
* @returns {ReturnType<_getODSTableRawContent>}
*/
export function getODSTableRawContent(odsArrBuff){
return _getODSTableRawContent(odsArrBuff, parseXML)
}
/** @type { typeof DOMImplementation.prototype.createDocument } */
const createDocument = function createDocument(...args){
// @ts-ignore
return document.implementation.createDocument(...args)
}
const serializer = new XMLSerializer()
/** @type { typeof XMLSerializer.prototype.serializeToString } */
const serializeToString = function serializeToString(node){
return serializer.serializeToString(node)
}
/**
* @param {ODTFile} odtTemplate
* @param {any} data
* @returns {Promise<ODTFile>}
*/
export function fillOdtTemplate(odtTemplate, data){
return _fillOdtTemplate(odtTemplate, data, parseXML, serializeToString, Node)
}
/**
* @param {Map<SheetName, SheetRawContent>} sheetsData
*/
export function createOdsFile(sheetsData){
return _createOdsFile(sheetsData, createDocument, serializeToString)
}
export {
// table-level exports
tableWithoutEmptyRows,
tableRawContentToValues,
tableRawContentToStrings,
tableRawContentToObjects,
// sheet-level exports
sheetRawContentToObjects,
sheetRawContentToStrings,
// row-level exports
rowRawContentToStrings,
isRowNotEmpty,
// cell-level exports
cellRawContentToStrings,
convertCellValue
} from './shared.js'

View File

@ -1,5 +1,8 @@
import { ZipWriter, BlobWriter, TextReader } from '@zip.js/zip.js'; import { ZipWriter, BlobWriter, TextReader } from '@zip.js/zip.js';
import {serializeToString, createDocument} from './DOMUtils.js'
/** @import {SheetCellRawContent, SheetName, SheetRawContent} from './types.js' */ /** @import {SheetCellRawContent, SheetName, SheetRawContent} from './types.js' */
const stylesXml = `<?xml version="1.0" encoding="UTF-8"?> const stylesXml = `<?xml version="1.0" encoding="UTF-8"?>
@ -22,11 +25,9 @@ const manifestXml = `<?xml version="1.0" encoding="UTF-8"?>
/** /**
* Crée un fichier .ods à partir d'un Map de feuilles de calcul * Crée un fichier .ods à partir d'un Map de feuilles de calcul
* @param {Map<SheetName, SheetRawContent>} sheetsData * @param {Map<SheetName, SheetRawContent>} sheetsData
* @param {typeof DOMImplementation.prototype.createDocument} createDocument
* @param {typeof XMLSerializer.prototype.serializeToString} serializeToString
* @returns {Promise<ArrayBuffer>} * @returns {Promise<ArrayBuffer>}
*/ */
export async function _createOdsFile(sheetsData, createDocument, serializeToString) { export async function createOdsFile(sheetsData) {
// Create a new zip writer // Create a new zip writer
const zipWriter = new ZipWriter(new BlobWriter('application/vnd.oasis.opendocument.spreadsheet')); const zipWriter = new ZipWriter(new BlobWriter('application/vnd.oasis.opendocument.spreadsheet'));
@ -44,7 +45,7 @@ export async function _createOdsFile(sheetsData, createDocument, serializeToStri
} }
); );
const contentXml = generateContentFileXMLString(sheetsData, createDocument, serializeToString); const contentXml = generateContentFileXMLString(sheetsData);
zipWriter.add("content.xml", new TextReader(contentXml), {level: 9}); zipWriter.add("content.xml", new TextReader(contentXml), {level: 9});
zipWriter.add("styles.xml", new TextReader(stylesXml)); zipWriter.add("styles.xml", new TextReader(stylesXml));
@ -60,11 +61,9 @@ export async function _createOdsFile(sheetsData, createDocument, serializeToStri
/** /**
* Generate the content.xml file with spreadsheet data * Generate the content.xml file with spreadsheet data
* @param {Map<SheetName, SheetRawContent>} sheetsData * @param {Map<SheetName, SheetRawContent>} sheetsData
* @param {typeof DOMImplementation.prototype.createDocument} createDocument
* @param {typeof XMLSerializer.prototype.serializeToString} serializeToString
* @returns {string} * @returns {string}
*/ */
function generateContentFileXMLString(sheetsData, createDocument, serializeToString) { function generateContentFileXMLString(sheetsData) {
const doc = createDocument('urn:oasis:names:tc:opendocument:xmlns:office:1.0', 'office:document-content'); const doc = createDocument('urn:oasis:names:tc:opendocument:xmlns:office:1.0', 'office:document-content');
const root = doc.documentElement; const root = doc.documentElement;

View File

@ -1,84 +0,0 @@
//@ts-check
import {DOMParser, DOMImplementation, XMLSerializer, Node} from '@xmldom/xmldom'
import {_getODSTableRawContent} from './shared.js'
import { _createOdsFile } from './createOdsFile.js'
import _fillOdtTemplate from './odf/fillOdtTemplate.js'
/** @import {SheetCellRawContent, SheetName, SheetRawContent} from './types.js' */
/** @import {ODTFile} from './odf/fillOdtTemplate.js' */
/**
*
* @param {string} str
* @returns {Document}
*/
function parseXML(str){
return (new DOMParser()).parseFromString(str, 'application/xml');
}
/**
* @param {ArrayBuffer} odsArrBuff
* @returns {ReturnType<_getODSTableRawContent>}
*/
export function getODSTableRawContent(odsArrBuff){
return _getODSTableRawContent(odsArrBuff, parseXML)
}
const implementation = new DOMImplementation()
/** @type { typeof DOMImplementation.prototype.createDocument } */
const createDocument = function createDocument(...args){
// @ts-ignore
return implementation.createDocument(...args)
}
const serializer = new XMLSerializer()
/** @type { typeof XMLSerializer.prototype.serializeToString } */
const serializeToString = function serializeToString(node){
return serializer.serializeToString(node)
}
/**
* @param {ODTFile} odtTemplate
* @param {any} data
* @returns {Promise<ODTFile>}
*/
export function fillOdtTemplate(odtTemplate, data){
return _fillOdtTemplate(odtTemplate, data, parseXML, serializeToString, Node)
}
/**
* @param {Map<SheetName, SheetRawContent>} sheetsData
*/
export function createOdsFile(sheetsData){
return _createOdsFile(sheetsData, createDocument, serializeToString)
}
export {
// table-level exports
tableWithoutEmptyRows,
tableRawContentToValues,
tableRawContentToStrings,
tableRawContentToObjects,
// sheet-level exports
sheetRawContentToObjects,
sheetRawContentToStrings,
// row-level exports
rowRawContentToStrings,
isRowNotEmpty,
// cell-level exports
cellRawContentToStrings,
convertCellValue
} from './shared.js'

View File

@ -1,6 +1,6 @@
import { ZipReader, ZipWriter, BlobReader, BlobWriter, TextReader, Uint8ArrayReader, TextWriter, Uint8ArrayWriter } from '@zip.js/zip.js'; import { ZipReader, ZipWriter, BlobReader, BlobWriter, TextReader, Uint8ArrayReader, TextWriter, Uint8ArrayWriter } from '@zip.js/zip.js';
import {traverse} from '../DOMUtils.js' import {traverse, parseXML, serializeToString, Node} from '../DOMUtils.js'
import {makeManifestFile, getManifestFileData} from './manifest.js'; import {makeManifestFile, getManifestFileData} from './manifest.js';
/** @import {Reader, ZipWriterAddDataOptions} from '@zip.js/zip.js' */ /** @import {Reader, ZipWriterAddDataOptions} from '@zip.js/zip.js' */
@ -344,12 +344,9 @@ function keepFile(filename){
/** /**
* @param {ODTFile} odtTemplate * @param {ODTFile} odtTemplate
* @param {any} data * @param {any} data
* @param {Function} parseXML
* @param {typeof XMLSerializer.prototype.serializeToString} serializeToString
* @param {typeof Node} Node
* @returns {Promise<ODTFile>} * @returns {Promise<ODTFile>}
*/ */
export default async function _fillOdtTemplate(odtTemplate, data, parseXML, serializeToString, Node) { export default async function fillOdtTemplate(odtTemplate, data) {
const reader = new ZipReader(new Uint8ArrayReader(new Uint8Array(odtTemplate))); const reader = new ZipReader(new Uint8ArrayReader(new Uint8Array(odtTemplate)));

View File

@ -0,0 +1,69 @@
import { ZipReader, Uint8ArrayReader, TextWriter } from '@zip.js/zip.js';
import {parseXML, Node} from '../../DOMUtils.js'
/** @import {ODTFile} from '../fillOdtTemplate.js' */
/**
* @param {ODTFile} odtFile
* @returns {Promise<Document>}
*/
async function getContentDocument(odtFile) {
const reader = new ZipReader(new Uint8ArrayReader(new Uint8Array(odtFile)));
const entries = await reader.getEntries();
const contentEntry = entries.find(entry => entry.filename === 'content.xml');
if (!contentEntry) {
throw new Error('No content.xml found in the ODT file');
}
// @ts-ignore
const contentText = await contentEntry.getData(new TextWriter());
await reader.close();
return parseXML(contentText)
}
/**
*
* @param {Document} odtDocument
* @returns {Element}
*/
function getODTTextElement(odtDocument) {
return odtDocument.getElementsByTagName('office:body')[0]
.getElementsByTagName('office:text')[0]
}
/**
* Extracts plain text content from an ODT file, preserving line breaks
* @param {ArrayBuffer} odtFile - The ODT file as an ArrayBuffer
* @returns {Promise<string>} Extracted text content
*/
export async function getOdtTextContent(odtFile) {
const contentDocument = await getContentDocument(odtFile)
const odtTextElement = getODTTextElement(contentDocument)
/**
*
* @param {Element} element
* @returns {string}
*/
function getElementTextContent(element){
//console.log('tagName', element.tagName)
if(element.tagName === 'text:h' || element.tagName === 'text:p')
return element.textContent + '\n'
else{
const descendantTexts = Array.from(element.childNodes)
.filter(n => n.nodeType === Node.ELEMENT_NODE)
.map(getElementTextContent)
if(element.tagName === 'text:list-item')
return `- ${descendantTexts.join('')}`
return descendantTexts.join('')
}
}
return getElementTextContent(odtTextElement)
}

View File

@ -1,23 +1,5 @@
import { readFile } from 'node:fs/promises' import { readFile } from 'node:fs/promises'
import { ZipReader, Uint8ArrayReader, TextWriter } from '@zip.js/zip.js';
import {DOMParser, Node} from '@xmldom/xmldom'
/** @import {ODTFile} from './fillOdtTemplate.js' */
/**
*
* @param {Document} odtDocument
* @returns {Element}
*/
function getODTTextElement(odtDocument) {
return odtDocument.getElementsByTagName('office:body')[0]
.getElementsByTagName('office:text')[0]
}
/** /**
* *
* @param {string} path * @param {string} path
@ -27,61 +9,3 @@ export async function getOdtTemplate(path) {
const fileBuffer = await readFile(path) const fileBuffer = await readFile(path)
return fileBuffer.buffer return fileBuffer.buffer
} }
/**
* Extracts plain text content from an ODT file, preserving line breaks
* @param {ArrayBuffer} odtFile - The ODT file as an ArrayBuffer
* @returns {Promise<string>} Extracted text content
*/
export async function getOdtTextContent(odtFile) {
const contentDocument = await getContentDocument(odtFile)
const odtTextElement = getODTTextElement(contentDocument)
/**
*
* @param {Element} element
* @returns {string}
*/
function getElementTextContent(element){
//console.log('tagName', element.tagName)
if(element.tagName === 'text:h' || element.tagName === 'text:p')
return element.textContent + '\n'
else{
const descendantTexts = Array.from(element.childNodes)
.filter(n => n.nodeType === Node.ELEMENT_NODE)
.map(getElementTextContent)
if(element.tagName === 'text:list-item')
return `- ${descendantTexts.join('')}`
return descendantTexts.join('')
}
}
return getElementTextContent(odtTextElement)
}
/**
* @param {ODTFile} odtFile
* @returns {Promise<Document>}
*/
async function getContentDocument(odtFile) {
const reader = new ZipReader(new Uint8ArrayReader(new Uint8Array(odtFile)));
const entries = await reader.getEntries();
const contentEntry = entries.find(entry => entry.filename === 'content.xml');
if (!contentEntry) {
throw new Error('No content.xml found in the ODT file');
}
// @ts-ignore
const contentText = await contentEntry.getData(new TextWriter());
await reader.close();
const parser = new DOMParser();
return parser.parseFromString(contentText, 'text/xml');
}

View File

@ -1,6 +1,8 @@
//@ts-check //@ts-check
import { Uint8ArrayReader, ZipReader, TextWriter } from '@zip.js/zip.js'; import { Uint8ArrayReader, ZipReader, TextWriter } from '@zip.js/zip.js';
import {parseXML} from './DOMUtils.js'
/** @import {Entry} from '@zip.js/zip.js'*/ /** @import {Entry} from '@zip.js/zip.js'*/
/** @import {SheetName, SheetRawContent, SheetRowRawContent, SheetCellRawContent} from './types.js' */ /** @import {SheetName, SheetRawContent, SheetRowRawContent, SheetCellRawContent} from './types.js' */
@ -46,10 +48,9 @@ function extraxtODSCellText(cell) {
/** /**
* Extracts raw table content from an ODS file. * Extracts raw table content from an ODS file.
* @param {ArrayBuffer} arrayBuffer - The ODS file. * @param {ArrayBuffer} arrayBuffer - The ODS file.
* @param {(str: string) => Document} parseXML - Function to parse XML content.
* @returns {Promise<Map<SheetName, SheetRawContent>>} * @returns {Promise<Map<SheetName, SheetRawContent>>}
*/ */
export async function _getODSTableRawContent(arrayBuffer, parseXML) { export async function getODSTableRawContent(arrayBuffer) {
const zipDataReader = new Uint8ArrayReader(new Uint8Array(arrayBuffer)); const zipDataReader = new Uint8ArrayReader(new Uint8Array(arrayBuffer));
const zipReader = new ZipReader(zipDataReader); const zipReader = new ZipReader(zipDataReader);
const zipEntries = await zipReader.getEntries() const zipEntries = await zipReader.getEntries()

View File

@ -2,7 +2,7 @@ import {readFile} from 'node:fs/promises'
import test from 'ava'; import test from 'ava';
import {getODSTableRawContent} from '../scripts/node.js' import {getODSTableRawContent} from '../exports.js'
const nomAgeContent = (await readFile('./tests/data/nom-age.ods')).buffer const nomAgeContent = (await readFile('./tests/data/nom-age.ods')).buffer

View File

@ -1,6 +1,6 @@
import test from 'ava'; import test from 'ava';
import {getODSTableRawContent, createOdsFile} from '../scripts/node.js' import {getODSTableRawContent, createOdsFile} from '../exports.js'
/** @import {SheetName, SheetRawContent} from '../scripts/types.js' */ /** @import {SheetName, SheetRawContent} from '../scripts/types.js' */

View File

@ -1,9 +1,9 @@
import test from 'ava'; import test from 'ava';
import {join} from 'node:path'; import {join} from 'node:path';
import {getOdtTemplate, getOdtTextContent} from '../scripts/odf/odtTemplate-forNode.js' import {getOdtTemplate} from '../scripts/odf/odtTemplate-forNode.js'
import {fillOdtTemplate} from '../scripts/node.js' import {fillOdtTemplate, getOdtTextContent} from '../exports.js'
import { listZipEntries } from './_helpers/zip-analysis.js'; import { listZipEntries } from './_helpers/zip-analysis.js';

View File

@ -2,7 +2,7 @@ import {readFile} from 'node:fs/promises'
import test from 'ava'; import test from 'ava';
import {getODSTableRawContent} from '../scripts/node.js' import {getODSTableRawContent} from '../exports.js'
test('.ods file with table:number-columns-repeated attribute in cell', async t => { test('.ods file with table:number-columns-repeated attribute in cell', async t => {
const repeatedCellFileContent = (await readFile('./tests/data/cellules-répétées.ods')).buffer const repeatedCellFileContent = (await readFile('./tests/data/cellules-répétées.ods')).buffer

View File

@ -1,5 +1,5 @@
import test from 'ava'; import test from 'ava';
import { sheetRawContentToObjects } from "../scripts/shared.js" import { sheetRawContentToObjects } from "../exports.js"
test("Empty header value should be kept", t => { test("Empty header value should be kept", t => {
const rawContent = [ const rawContent = [