From 42a969325c052b9b1927291b4d6a15172298e21f Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Mon, 6 May 2024 00:26:04 +0200 Subject: [PATCH 01/16] fix missing types and jsDocs --- src/index.js | 4 ++-- src/mocompiler.js | 21 ++++++++++----------- src/moparser.js | 26 ++++++++++++++++---------- src/pocompiler.js | 20 +++++++++++--------- src/poparser.js | 24 +++++++++++++----------- src/shared.js | 8 +++++--- src/{index.d.ts => types.d.ts} | 8 +++----- 7 files changed, 60 insertions(+), 51 deletions(-) rename src/{index.d.ts => types.d.ts} (89%) diff --git a/src/index.js b/src/index.js index 05e32a3..b115f17 100644 --- a/src/index.js +++ b/src/index.js @@ -7,7 +7,7 @@ import moCompiler from './mocompiler.js'; * Translation parser and compiler for PO files * @see https://www.gnu.org/software/gettext/manual/html_node/PO.html * - * @type {import("./index.d.ts").po} po + * @type {import("./types.d.ts").po} po */ export const po = { parse: poParser.parse, @@ -19,7 +19,7 @@ export const po = { * Translation parser and compiler for PO files * @see https://www.gnu.org/software/gettext/manual/html_node/MO.html * - * @type {import("./index.d.ts").mo} mo + * @type {import("./types.d.ts").mo} mo */ export const mo = { parse: moParser, diff --git a/src/mocompiler.js b/src/mocompiler.js index 229904d..3a3c3c6 100644 --- a/src/mocompiler.js +++ b/src/mocompiler.js @@ -6,7 +6,7 @@ import contentType from 'content-type'; * Exposes general compiler function. Takes a translation * object as a parameter and returns binary MO object * - * @param {import('./index.d.ts').GetTextTranslations} table Translation object + * @param {import('./types.d.ts').GetTextTranslations} table Translation object * @return {Buffer} Compiled binary MO object */ export default function (table) { @@ -19,8 +19,8 @@ export default function (table) { * Creates a MO compiler object. * * @constructor - * @param {import('./index.d.ts').GetTextTranslations} table Translation table as defined in the README - * @return {import('./index.d.ts').Compiler} Compiler + * @param {import('./types.d.ts').GetTextTranslations} table Translation table as defined in the README + * @return {import('./types.d.ts').Compiler} Compiler */ function Compiler (table = {}) { this._table = table; @@ -96,7 +96,7 @@ Compiler.prototype._handleCharset = function () { /** * Generates an array of translation strings - * in the form of [{msgid:... , msgstr:...}] + * in the form of [{msgid:..., msgstr: ...}] * * @return {Array} Translation strings array */ @@ -148,20 +148,19 @@ Compiler.prototype._generateList = function () { /** * Calculate buffer size for the final binary object * - * @param {import('./index.d.ts').GetTextTranslations} list An array of translation strings from _generateList + * @param {import('./types.d.ts').GetTextTranslation[]} list An array of translation strings from _generateList * @return {Object} Size data of {msgid, msgstr, total} */ Compiler.prototype._calculateSize = function (list) { let msgidLength = 0; let msgstrLength = 0; - let totalLength = 0; list.forEach(translation => { msgidLength += translation.msgid.length + 1; // + extra 0x00 msgstrLength += translation.msgstr.length + 1; // + extra 0x00 }); - totalLength = 4 + // magic number + let totalLength = 4 + // magic number 4 + // revision 4 + // string count 4 + // original string table offset @@ -183,7 +182,7 @@ Compiler.prototype._calculateSize = function (list) { /** * Generates the binary MO object from the translation list * - * @param {import('./index.d.ts').GetTextTranslations} list translation list + * @param {import('./types.d.ts').GetTextTranslation[]} list translation list * @param {Object} size Byte size information * @return {Buffer} Compiled MO object */ @@ -214,7 +213,7 @@ Compiler.prototype._build = function (list, size) { // hash table offset returnBuffer[this._writeFunc](28 + (4 + 4) * list.length * 2, 24); - // build originals table + // Build originals table curPosition = 28 + 2 * (4 + 4) * list.length; for (i = 0, len = list.length; i < len; i++) { list[i].msgid.copy(returnBuffer, curPosition); @@ -224,7 +223,7 @@ Compiler.prototype._build = function (list, size) { curPosition += list[i].msgid.length + 1; } - // build translations table + // build translation table for (i = 0, len = list.length; i < len; i++) { list[i].msgstr.copy(returnBuffer, curPosition); returnBuffer[this._writeFunc](list[i].msgstr.length, 28 + (4 + 4) * list.length + i * 8); @@ -237,7 +236,7 @@ Compiler.prototype._build = function (list, size) { }; /** - * Compiles translation object into a binary MO object + * Compiles a translation object into a binary MO object * * @return {Buffer} Compiled MO object */ diff --git a/src/moparser.js b/src/moparser.js index 43097cf..c7d3a16 100644 --- a/src/moparser.js +++ b/src/moparser.js @@ -36,6 +36,12 @@ function Parser (fileContents, defaultCharset = 'iso-8859-1') { this._charset = defaultCharset; + /** + * Translation table + * + * @type {import( './shared.js').GetTextTranslations} table Translation object + * @private + */ this._table = { charset: this._charset, headers: undefined, @@ -49,7 +55,7 @@ function Parser (fileContents, defaultCharset = 'iso-8859-1') { Parser.prototype.MAGIC = 0x950412de; /** - * Checks if number values in the input file are in big- or littleendian format. + * Checks if number values in the input file are in big- or little endian format. * * @return {Boolean} Return true if magic was detected */ @@ -70,8 +76,8 @@ Parser.prototype._checkMagick = function () { }; /** - * Read the original strings and translations from the input MO file. Use the - * first translation string in the file as the header. + * Read the original strings and translations from the input MO file. + * Use the first translation string in the file as the header. */ Parser.prototype._loadTranslationTable = function () { let offsetOriginals = this._offsetOriginals; @@ -121,7 +127,7 @@ Parser.prototype._loadTranslationTable = function () { /** * Detects charset for MO strings from the header * - * @param {Buffer} headers Header value + * @param {string} headers Header value */ Parser.prototype._handleCharset = function (headers) { const headersStr = headers.toString(); @@ -140,15 +146,15 @@ Parser.prototype._handleCharset = function (headers) { /** * Adds a translation to the translation object * - * @param {String} msgid Original string - * @params {String} msgstr Translation for the original string + * @param {String} msgidRaw Original string + * @param {String} msgstrRaw Translation for the original string */ -Parser.prototype._addString = function (msgid, msgstr) { +Parser.prototype._addString = function (msgidRaw, msgstrRaw) { const translation = {}; let msgctxt; let msgidPlural; - msgid = msgid.split('\u0004'); + let msgid = msgidRaw.split('\u0004'); if (msgid.length > 1) { msgctxt = msgid.shift(); translation.msgctxt = msgctxt; @@ -166,7 +172,7 @@ Parser.prototype._addString = function (msgid, msgstr) { translation.msgid_plural = msgidPlural; } - msgstr = msgstr.split('\u0000'); + let msgstr = msgstrRaw.split('\u0000'); translation.msgstr = [].concat(msgstr || []); if (!this._table.translations[msgctxt]) { @@ -179,7 +185,7 @@ Parser.prototype._addString = function (msgid, msgstr) { /** * Parses the MO object and returns translation table * - * @return {Object} Translation table + * @return {import("./types.d.ts").GetTextTranslations | false} Translation table */ Parser.prototype.parse = function () { if (!this._checkMagick()) { diff --git a/src/pocompiler.js b/src/pocompiler.js index 024492c..78185eb 100644 --- a/src/pocompiler.js +++ b/src/pocompiler.js @@ -6,8 +6,9 @@ import contentType from 'content-type'; * Exposes general compiler function. Takes a translation * object as a parameter and returns PO object * - * @param {Object} table Translation object - * @return {Buffer} Compiled PO object + * @param {import( './types.d.ts').GettextTranslations} table Translation object + * @param {import( './types.d.ts').parserOptions|{}} [options] Options + * @return {Buffer} The compiled PO object */ export default function (table, options) { const compiler = new Compiler(table, options); @@ -19,7 +20,8 @@ export default function (table, options) { * Creates a PO compiler object. * * @constructor - * @param {Object} table Translation table to be compiled + * @param {import( './types.d.ts').GetTextTranslations|{}} table Translation table to be compiled + * @param {import( './types.d.ts').parserOptions|{}} [options] Options */ function Compiler (table = {}, options = {}) { this._table = table; @@ -65,10 +67,10 @@ function Compiler (table = {}, options = {}) { } /** - * Converts a comments object to a comment string. The comment object is - * in the form of {translator:'', reference: '', extracted: '', flag: '', previous:''} + * Converts a comment object to a comment string. The comment object is + * in the form of {translator: '', reference: '', extracted: '', flag: '', previous: ''} * - * @param {Object} comments A comments object + * @param {import(types.d.ts).GetTextComment} comments A comments object * @return {String} A comment string for the PO file */ Compiler.prototype._drawComments = function (comments) { @@ -106,7 +108,7 @@ Compiler.prototype._drawComments = function (comments) { /** * Builds a PO string for a single translation object * - * @param {Object} block Translation object + * @param {import(types.d.ts).GetTextTranslation} block Translation object * @param {Object} [override] Properties of this object will override `block` properties * @param {boolean} [obsolete] Block is obsolete and must be commented out * @return {String} Translation string for a single object @@ -256,9 +258,9 @@ Compiler.prototype._prepareSection = function (section) { }; /** - * Compiles translation object into a PO object + * Compiles a translation object into a PO object * - * @return {Buffer} Compiled PO object + * @return {Buffer} Compiled a PO object */ Compiler.prototype.compile = function () { const headerBlock = (this._table.translations[''] && this._table.translations['']['']) || {}; diff --git a/src/poparser.js b/src/poparser.js index 981dcca..4caef0e 100644 --- a/src/poparser.js +++ b/src/poparser.js @@ -28,8 +28,8 @@ export function stream (options = {}, transformOptions = {}) { } /** - * Creates a PO parser object. If PO object is a string, - * UTF-8 will be used as the charset + * Creates a PO parser object. + * If a PO object is a string, UTF-8 will be used as the charset * * @typedef {{ defaultCharset?: string, validation?: boolean }} Options * @constructor @@ -68,7 +68,7 @@ Parser.prototype.parse = function () { /** * Detects charset for PO strings from the header * - * @param {Buffer} headers Header value + * @param {string | Buffer} buf Header value */ Parser.prototype._handleCharset = function (buf = '') { const str = buf.toString(); @@ -410,11 +410,11 @@ Parser.prototype._handleValues = function (tokens) { /** * Validate token * - * @param {Object} token Parsed token - * @param {Object} translations Translation table + * @param {{ msgid?: string, msgid_plural?: string, msgstr?: string[] }} token Parsed token + * @param {import("./types.d.ts").GetTextTranslation[]} translations Translation table * @param {string} msgctxt Message entry context - * @param {number} nplurals Number of epected plural forms - * @throws Will throw an error if token validation fails + * @param {number} nplurals Number of expected plural forms + * @throws {Error} Will throw an error if token validation fails */ Parser.prototype._validateToken = function ( { @@ -513,9 +513,7 @@ Parser.prototype._finalize = function (tokens) { /** * Creates a transform stream for parsing PO input * - * @typedef {{ defaultCharset: string, validation: boolean }} Options - * @constructor - * @param {Options} options Optional options with defaultCharset and validation + * @param {import( "./types.d.ts").parserOptions} options Optional options with defaultCharset and validation * @param {import('readable-stream').TransformOptions} transformOptions Optional stream options */ function PoParserTransform (options, transformOptions) { @@ -536,6 +534,9 @@ util.inherits(PoParserTransform, Transform); /** * Processes a chunk of the input stream + * @param {Buffer} chunk Chunk of the input stream + * @param {string} encoding Encoding of the chunk + * @param {Function} done Callback to call when the chunk is processed */ PoParserTransform.prototype._transform = function (chunk, encoding, done) { let i; @@ -601,7 +602,8 @@ PoParserTransform.prototype._transform = function (chunk, encoding, done) { }; /** - * Once all input has been processed emit the parsed translation table as an object + * Once all inputs have been processed, emit the parsed translation table as an object + * @param {Function} done Callback to call when the chunk is processed */ PoParserTransform.prototype._flush = function (done) { let chunk; diff --git a/src/shared.js b/src/shared.js index 5e2d3a5..d9238c4 100644 --- a/src/shared.js +++ b/src/shared.js @@ -43,6 +43,7 @@ export function parseHeader (str = '') { * Attempts to safely parse 'nplurals" value from "Plural-Forms" header * * @param {Object} [headers = {}] An object with parsed headers + * @param {number} fallback Fallback value if "Plural-Forms" header is absent * @returns {number} Parsed result */ export function parseNPluralFromHeadersSafely (headers, fallback = 1) { @@ -83,6 +84,7 @@ export function generateHeader (header = {}) { * Normalizes charset name. Converts utf8 to utf-8, WIN1257 to windows-1257 etc. * * @param {String} charset Charset name + * @param {String} defaultCharset Default charset name, defaults to 'iso-8859-1' * @return {String} Normalized charset name */ export function formatCharset (charset = 'iso-8859-1', defaultCharset = 'iso-8859-1') { @@ -125,7 +127,7 @@ export function foldLine (str, maxLen = 76) { curLine = match[0]; } else if (pos + curLine.length < len) { // if we're not at the end - if ((match = /.*\s+/.exec(curLine)) && /[^\s]/.test(match[0])) { + if ((match = /.*\s+/.exec(curLine)) && /\S/.test(match[0])) { // use everything before and including the last white space character (if anything) curLine = match[0]; } else if ((match = /.*[\x21-\x2f0-9\x5b-\x60\x7b-\x7e]+/.exec(curLine)) && /[^\x21-\x2f0-9\x5b-\x60\x7b-\x7e]/.test(match[0])) { @@ -144,8 +146,8 @@ export function foldLine (str, maxLen = 76) { /** * Comparator function for comparing msgid * - * @param {Object} object with msgid prev - * @param {Object} object with msgid next + * @param {Object} left with msgid prev + * @param {Object} right with msgid next * @returns {number} comparator index */ export function compareMsgid ({ msgid: left }, { msgid: right }) { diff --git a/src/index.d.ts b/src/types.d.ts similarity index 89% rename from src/index.d.ts rename to src/types.d.ts index 8adf7ea..4eac77f 100644 --- a/src/index.d.ts +++ b/src/types.d.ts @@ -1,8 +1,6 @@ import { Transform } from "readable-stream"; -export declare module 'encoding' { - export function convert(buf: Buffer, toCharset: string, fromCharset: string): Buffer; -} +declare module 'encoding' export interface Compiler { _table: GetTextTranslations; @@ -47,6 +45,6 @@ export interface mo { compile: (table: GetTextTranslations, options?: parserOptions) => Buffer; } -export * from "./@types"; - export default { po, mo } as { po: po, mo: mo }; + +export * from './types.d.ts'; From cbe6e8f05e6455ea79023b12af39d20077b6c1d2 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Wed, 8 May 2024 21:20:25 +0200 Subject: [PATCH 02/16] removing definition file, the definitions were moved to the type.js file --- .eslintrc.json | 3 +-- package.json | 2 -- src/index.js | 6 +----- src/mocompiler.js | 13 ++++++------ src/moparser.js | 8 ++++---- src/pocompiler.js | 17 +++++++++------- src/poparser.js | 6 +++--- src/types.d.ts | 50 ----------------------------------------------- src/types.js | 34 ++++++++++++++++++++++++++++++++ tsconfig.json | 3 +-- 10 files changed, 61 insertions(+), 81 deletions(-) delete mode 100644 src/types.d.ts create mode 100644 src/types.js diff --git a/.eslintrc.json b/.eslintrc.json index bcfae6d..30149ce 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -3,8 +3,7 @@ "es2021": true, "node": true }, - "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], - "parser": "@typescript-eslint/parser", + "extends": "standard", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" diff --git a/package.json b/package.json index e0115d5..5342a40 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,6 @@ "@types/content-type": "^1.1.8", "@types/mocha": "latest", "@types/readable-stream": "^4.0.11", - "@typescript-eslint/eslint-plugin": "^6.18.1", - "@typescript-eslint/parser": "^6.14.0", "chai": "^5.0.3", "eslint": "^8.56.0", "eslint-config-standard": "^17.1.0", diff --git a/src/index.js b/src/index.js index b115f17..cf6ae07 100644 --- a/src/index.js +++ b/src/index.js @@ -6,8 +6,6 @@ import moCompiler from './mocompiler.js'; /** * Translation parser and compiler for PO files * @see https://www.gnu.org/software/gettext/manual/html_node/PO.html - * - * @type {import("./types.d.ts").po} po */ export const po = { parse: poParser.parse, @@ -16,10 +14,8 @@ export const po = { }; /** - * Translation parser and compiler for PO files + * Translation parser and compiler for MO files * @see https://www.gnu.org/software/gettext/manual/html_node/MO.html - * - * @type {import("./types.d.ts").mo} mo */ export const mo = { parse: moParser, diff --git a/src/mocompiler.js b/src/mocompiler.js index 3a3c3c6..f959924 100644 --- a/src/mocompiler.js +++ b/src/mocompiler.js @@ -6,7 +6,7 @@ import contentType from 'content-type'; * Exposes general compiler function. Takes a translation * object as a parameter and returns binary MO object * - * @param {import('./types.d.ts').GetTextTranslations} table Translation object + * @param {import('./types.js').GetTextTranslations} table Translation object * @return {Buffer} Compiled binary MO object */ export default function (table) { @@ -19,8 +19,8 @@ export default function (table) { * Creates a MO compiler object. * * @constructor - * @param {import('./types.d.ts').GetTextTranslations} table Translation table as defined in the README - * @return {import('./types.d.ts').Compiler} Compiler + * @param {import('./types.js').GetTextTranslations} table Translation table as defined in the README + * @return {import('./types.js').Compiler} Compiler */ function Compiler (table = {}) { this._table = table; @@ -148,7 +148,7 @@ Compiler.prototype._generateList = function () { /** * Calculate buffer size for the final binary object * - * @param {import('./types.d.ts').GetTextTranslation[]} list An array of translation strings from _generateList + * @param {import('./types.js').GetTextTranslation[]} list An array of translation strings from _generateList * @return {Object} Size data of {msgid, msgstr, total} */ Compiler.prototype._calculateSize = function (list) { @@ -160,7 +160,7 @@ Compiler.prototype._calculateSize = function (list) { msgstrLength += translation.msgstr.length + 1; // + extra 0x00 }); - let totalLength = 4 + // magic number + const totalLength = 4 + // magic number 4 + // revision 4 + // string count 4 + // original string table offset @@ -182,7 +182,7 @@ Compiler.prototype._calculateSize = function (list) { /** * Generates the binary MO object from the translation list * - * @param {import('./types.d.ts').GetTextTranslation[]} list translation list + * @param {import('./types.js').GetTextTranslation[]} list translation list * @param {Object} size Byte size information * @return {Buffer} Compiled MO object */ @@ -238,6 +238,7 @@ Compiler.prototype._build = function (list, size) { /** * Compiles a translation object into a binary MO object * + * @interface * @return {Buffer} Compiled MO object */ Compiler.prototype.compile = function () { diff --git a/src/moparser.js b/src/moparser.js index c7d3a16..801d6f5 100644 --- a/src/moparser.js +++ b/src/moparser.js @@ -95,7 +95,7 @@ Parser.prototype._loadTranslationTable = function () { offsetOriginals += 4; msgid = this._fileContents.subarray( position, - position + length, + position + length ); // matching msgstr @@ -105,7 +105,7 @@ Parser.prototype._loadTranslationTable = function () { offsetTranslations += 4; msgstr = this._fileContents.subarray( position, - position + length, + position + length ); if (!i && !msgid.toString()) { @@ -172,7 +172,7 @@ Parser.prototype._addString = function (msgidRaw, msgstrRaw) { translation.msgid_plural = msgidPlural; } - let msgstr = msgstrRaw.split('\u0000'); + const msgstr = msgstrRaw.split('\u0000'); translation.msgstr = [].concat(msgstr || []); if (!this._table.translations[msgctxt]) { @@ -185,7 +185,7 @@ Parser.prototype._addString = function (msgidRaw, msgstrRaw) { /** * Parses the MO object and returns translation table * - * @return {import("./types.d.ts").GetTextTranslations | false} Translation table + * @return {import("./index.js").GetTextTranslations | false} Translation table */ Parser.prototype.parse = function () { if (!this._checkMagick()) { diff --git a/src/pocompiler.js b/src/pocompiler.js index 78185eb..510b435 100644 --- a/src/pocompiler.js +++ b/src/pocompiler.js @@ -1,13 +1,15 @@ -import encoding from 'encoding'; import { HEADERS, foldLine, compareMsgid, formatCharset, generateHeader } from './shared.js'; import contentType from 'content-type'; +// @ts-expect-error TS7016: Could not find a declaration file for module encoding. +import encoding from 'encoding'; + /** * Exposes general compiler function. Takes a translation * object as a parameter and returns PO object * - * @param {import( './types.d.ts').GettextTranslations} table Translation object - * @param {import( './types.d.ts').parserOptions|{}} [options] Options + * @param {import('./types.js').GettextTranslations} table Translation object + * @param {import('./types.js').parserOptions|{}} [options] Options * @return {Buffer} The compiled PO object */ export default function (table, options) { @@ -20,8 +22,8 @@ export default function (table, options) { * Creates a PO compiler object. * * @constructor - * @param {import( './types.d.ts').GetTextTranslations|{}} table Translation table to be compiled - * @param {import( './types.d.ts').parserOptions|{}} [options] Options + * @param {import('./types.js').GetTextTranslations|{}} table Translation table to be compiled + * @param {import('./types.js').parserOptions|{}} [options] Options */ function Compiler (table = {}, options = {}) { this._table = table; @@ -70,7 +72,7 @@ function Compiler (table = {}, options = {}) { * Converts a comment object to a comment string. The comment object is * in the form of {translator: '', reference: '', extracted: '', flag: '', previous: ''} * - * @param {import(types.d.ts).GetTextComment} comments A comments object + * @param {import('./types.js').GetTextComment} comments A comments object * @return {String} A comment string for the PO file */ Compiler.prototype._drawComments = function (comments) { @@ -108,7 +110,7 @@ Compiler.prototype._drawComments = function (comments) { /** * Builds a PO string for a single translation object * - * @param {import(types.d.ts).GetTextTranslation} block Translation object + * @param {import('./types.js').GetTextTranslation} block Translation object * @param {Object} [override] Properties of this object will override `block` properties * @param {boolean} [obsolete] Block is obsolete and must be commented out * @return {String} Translation string for a single object @@ -260,6 +262,7 @@ Compiler.prototype._prepareSection = function (section) { /** * Compiles a translation object into a PO object * + * @interface * @return {Buffer} Compiled a PO object */ Compiler.prototype.compile = function () { diff --git a/src/poparser.js b/src/poparser.js index 4caef0e..129cda7 100644 --- a/src/poparser.js +++ b/src/poparser.js @@ -152,7 +152,7 @@ Parser.prototype._lexer = function (chunk) { }; this._lex.push(this._node); this._state = this.states.string; - } else if (chr === "#") { + } else if (chr === '#') { this._node = { type: this.types.comments, value: '' @@ -411,7 +411,7 @@ Parser.prototype._handleValues = function (tokens) { * Validate token * * @param {{ msgid?: string, msgid_plural?: string, msgstr?: string[] }} token Parsed token - * @param {import("./types.d.ts").GetTextTranslation[]} translations Translation table + * @param {import("./index.js").GetTextTranslation[]} translations Translation table * @param {string} msgctxt Message entry context * @param {number} nplurals Number of expected plural forms * @throws {Error} Will throw an error if token validation fails @@ -513,7 +513,7 @@ Parser.prototype._finalize = function (tokens) { /** * Creates a transform stream for parsing PO input * - * @param {import( "./types.d.ts").parserOptions} options Optional options with defaultCharset and validation + * @param {import( "./index.js").parserOptions} options Optional options with defaultCharset and validation * @param {import('readable-stream').TransformOptions} transformOptions Optional stream options */ function PoParserTransform (options, transformOptions) { diff --git a/src/types.d.ts b/src/types.d.ts deleted file mode 100644 index 4eac77f..0000000 --- a/src/types.d.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Transform } from "readable-stream"; - -declare module 'encoding' - -export interface Compiler { - _table: GetTextTranslations; - compile(): Buffer; -} - -export interface GetTextComment { - translator?: string; - reference?: string; - extracted?: string; - flag?: string; - previous?: string; -} - -export interface GetTextTranslation { - msgctxt?: string; - msgid: string; - msgid_plural?: string; - msgstr: string[]; - comments?: GetTextComment; -} - -export interface GetTextTranslations { - charset: string; - headers: { [headerName: string]: string }; - translations: { [msgctxt: string]: { [msgId: string]: GetTextTranslation } }; -} - -export interface parserOptions { - defaultCharset?: string; - validation?: boolean; -} - -export interface po { - parse: (buffer: Buffer | string, defaultCharset?: string) => GetTextTranslations; - compile: (table: GetTextTranslations, options?: parserOptions) => Buffer; - createParseStream: (options?: parserOptions, transformOptions?: import('readable-stream').TransformOptions) => Transform; -} - -export interface mo { - parse: (buffer: Buffer | string, defaultCharset?: string) => GetTextTranslations; - compile: (table: GetTextTranslations, options?: parserOptions) => Buffer; -} - -export default { po, mo } as { po: po, mo: mo }; - -export * from './types.d.ts'; diff --git a/src/types.js b/src/types.js new file mode 100644 index 0000000..269befa --- /dev/null +++ b/src/types.js @@ -0,0 +1,34 @@ +/** + * Represents a GetText comment. + * @typedef {Object} GetTextComment + * @property {string} [translator] Translator information. + * @property {string} [reference] Reference information. + * @property {string} [extracted] Extracted comments. + * @property {string} [flag] Flags. + * @property {string} [previous] Previous string. + */ + +/** + * Represents a GetText translation. + * @typedef {Object} GetTextTranslation + * @property {string} [msgctxt] Context of the message. + * @property {string} msgid The singular message ID. + * @property {string} [msgid_plural] The plural message ID. + * @property {string[]} msgstr Array of translated strings. + * @property {GetTextComment} [comments] Comments associated with the translation. + */ + +/** + * Represents GetText translations. + * @typedef {Object} GetTextTranslations + * @property {string} charset Character set. + * @property {Object.} headers Headers. + * @property {Object.>} translations Translations. + */ + +/** + * Options for the parser. + * @typedef {Object} parserOptions + * @property {string} [defaultCharset] Default character set. + * @property {boolean} [validation] Whether to perform validation. + */ diff --git a/tsconfig.json b/tsconfig.json index 96e5c6b..5dd7090 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,7 +31,6 @@ "skipLibCheck": true }, "include": [ - "src/**/*.js", - "index.d.ts" + "src/**/*", ] } From a9e6fa0b903710753285d2f7dd70feda50d66a57 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Wed, 8 May 2024 21:39:38 +0200 Subject: [PATCH 03/16] enhanced type for pocompiler and postream --- src/index.js | 6 +++--- src/poparser.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index cf6ae07..fe34104 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import * as poParser from './poparser.js'; +import { poParse, poStream } from './poparser.js'; import poCompiler from './pocompiler.js'; import moParser from './moparser.js'; import moCompiler from './mocompiler.js'; @@ -8,8 +8,8 @@ import moCompiler from './mocompiler.js'; * @see https://www.gnu.org/software/gettext/manual/html_node/PO.html */ export const po = { - parse: poParser.parse, - createParseStream: poParser.stream, + parse: poParse, + createParseStream: poStream, compile: poCompiler }; diff --git a/src/poparser.js b/src/poparser.js index 129cda7..f6e251b 100644 --- a/src/poparser.js +++ b/src/poparser.js @@ -10,7 +10,7 @@ import util from 'util'; * @param {string | Buffer} input PO object * @param {Options} [options] Optional options with defaultCharset and validation */ -export function parse (input, options = {}) { +export function poParse (input, options = {}) { const parser = new Parser(input, options); return parser.parse(); @@ -23,7 +23,7 @@ export function parse (input, options = {}) { * @param {Options} [options] Optional options with defaultCharset and validation * @param {import('readable-stream').TransformOptions} [transformOptions] Optional stream options */ -export function stream (options = {}, transformOptions = {}) { +export function poStream (options = {}, transformOptions = {}) { return new PoParserTransform(options, transformOptions); } From 7f94d007e3a6d3f8b05180d6d71d9337fcdadd8b Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Thu, 9 May 2024 00:14:16 +0200 Subject: [PATCH 04/16] enhancing js docs types --- src/mocompiler.js | 9 ++++----- src/moparser.js | 6 +++--- src/pocompiler.js | 6 +++--- src/poparser.js | 4 ++-- src/shared.js | 4 ++-- 5 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/mocompiler.js b/src/mocompiler.js index f959924..5d352d7 100644 --- a/src/mocompiler.js +++ b/src/mocompiler.js @@ -19,10 +19,9 @@ export default function (table) { * Creates a MO compiler object. * * @constructor - * @param {import('./types.js').GetTextTranslations} table Translation table as defined in the README - * @return {import('./types.js').Compiler} Compiler + * @param {import('./types.js').GetTextTranslations|{headers: {}, translations: {}}} table Translation table as defined in the README */ -function Compiler (table = {}) { +function Compiler (table = {headers: {}, translations: {}}) { this._table = table; let { headers = {}, translations = {} } = this._table; @@ -98,7 +97,7 @@ Compiler.prototype._handleCharset = function () { * Generates an array of translation strings * in the form of [{msgid:..., msgstr: ...}] * - * @return {Array} Translation strings array + * @return {import('./types.js').GetTextTranslation[]} Translation strings array */ Compiler.prototype._generateList = function () { const list = []; @@ -183,7 +182,7 @@ Compiler.prototype._calculateSize = function (list) { * Generates the binary MO object from the translation list * * @param {import('./types.js').GetTextTranslation[]} list translation list - * @param {Object} size Byte size information + * @param {Buffer} size Byte size information * @return {Buffer} Compiled MO object */ Compiler.prototype._build = function (list, size) { diff --git a/src/moparser.js b/src/moparser.js index 801d6f5..9618dbe 100644 --- a/src/moparser.js +++ b/src/moparser.js @@ -39,7 +39,7 @@ function Parser (fileContents, defaultCharset = 'iso-8859-1') { /** * Translation table * - * @type {import( './shared.js').GetTextTranslations} table Translation object + * @type {import('./types.js').GetTextTranslations} table Translation object * @private */ this._table = { @@ -127,7 +127,7 @@ Parser.prototype._loadTranslationTable = function () { /** * Detects charset for MO strings from the header * - * @param {string} headers Header value + * @param {String} headers Header value */ Parser.prototype._handleCharset = function (headers) { const headersStr = headers.toString(); @@ -185,7 +185,7 @@ Parser.prototype._addString = function (msgidRaw, msgstrRaw) { /** * Parses the MO object and returns translation table * - * @return {import("./index.js").GetTextTranslations | false} Translation table + * @return {import("./types.js").GetTextTranslations | false} Translation table */ Parser.prototype.parse = function () { if (!this._checkMagick()) { diff --git a/src/pocompiler.js b/src/pocompiler.js index 510b435..f6aafea 100644 --- a/src/pocompiler.js +++ b/src/pocompiler.js @@ -8,7 +8,7 @@ import encoding from 'encoding'; * Exposes general compiler function. Takes a translation * object as a parameter and returns PO object * - * @param {import('./types.js').GettextTranslations} table Translation object + * @param {import('./types.js').GetTextTranslations} table Translation object * @param {import('./types.js').parserOptions|{}} [options] Options * @return {Buffer} The compiled PO object */ @@ -111,7 +111,7 @@ Compiler.prototype._drawComments = function (comments) { * Builds a PO string for a single translation object * * @param {import('./types.js').GetTextTranslation} block Translation object - * @param {Object} [override] Properties of this object will override `block` properties + * @param {import('./types.js').GetTextTranslation|{}} [override] Properties of this object will override `block` properties * @param {boolean} [obsolete] Block is obsolete and must be commented out * @return {String} Translation string for a single object */ @@ -223,7 +223,7 @@ Compiler.prototype._handleCharset = function () { * Flatten and sort translations object * * @param {Object} section Object to be prepared (translations or obsolete) - * @returns {Array} Prepared array + * @returns {string[]} Prepared array */ Compiler.prototype._prepareSection = function (section) { let response = []; diff --git a/src/poparser.js b/src/poparser.js index f6e251b..6b48144 100644 --- a/src/poparser.js +++ b/src/poparser.js @@ -411,7 +411,7 @@ Parser.prototype._handleValues = function (tokens) { * Validate token * * @param {{ msgid?: string, msgid_plural?: string, msgstr?: string[] }} token Parsed token - * @param {import("./index.js").GetTextTranslation[]} translations Translation table + * @param {import("./types.js").GetTextTranslation} translations Translation table * @param {string} msgctxt Message entry context * @param {number} nplurals Number of expected plural forms * @throws {Error} Will throw an error if token validation fails @@ -513,7 +513,7 @@ Parser.prototype._finalize = function (tokens) { /** * Creates a transform stream for parsing PO input * - * @param {import( "./index.js").parserOptions} options Optional options with defaultCharset and validation + * @param {import( "./types.js").parserOptions} options Optional options with defaultCharset and validation * @param {import('readable-stream').TransformOptions} transformOptions Optional stream options */ function PoParserTransform (options, transformOptions) { diff --git a/src/shared.js b/src/shared.js index d9238c4..cfe3abe 100644 --- a/src/shared.js +++ b/src/shared.js @@ -146,8 +146,8 @@ export function foldLine (str, maxLen = 76) { /** * Comparator function for comparing msgid * - * @param {Object} left with msgid prev - * @param {Object} right with msgid next + * @param {{msgid: string}} left with msgid prev + * @param {{msgid: string}} right with msgid next * @returns {number} comparator index */ export function compareMsgid ({ msgid: left }, { msgid: right }) { From f4366dd0a6c46cfa2622535174e1980023c87798 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Sat, 11 May 2024 14:28:15 +0200 Subject: [PATCH 05/16] jsdocs types (still few error to solve) --- .github/workflows/ci.yml | 1 + src/mocompiler.js | 107 ++++++++++++++----------- src/moparser.js | 147 ++++++++++++++++------------------ src/pocompiler.js | 165 ++++++++++++++++++++++----------------- src/poparser.js | 159 ++++++++++++++++++++++++------------- src/shared.js | 18 +++-- src/types.js | 12 ++- tsconfig.json | 2 +- 8 files changed, 351 insertions(+), 260 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9056d3..77f0543 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,4 +21,5 @@ jobs: with: node-version: ${{ matrix.node-version }} - run: npm install + - run: npx tsc - run: npm test diff --git a/src/mocompiler.js b/src/mocompiler.js index 5d352d7..e8a54a5 100644 --- a/src/mocompiler.js +++ b/src/mocompiler.js @@ -16,23 +16,21 @@ export default function (table) { } /** - * Creates a MO compiler object. - * - * @constructor - * @param {import('./types.js').GetTextTranslations|{headers: {}, translations: {}}} table Translation table as defined in the README + * Prepare the header object to be compatible with MO compiler + * @param {{[key: string]: string}} headers the headers + * @return {{[key: string]: string}} The prepared header */ -function Compiler (table = {headers: {}, translations: {}}) { - this._table = table; - - let { headers = {}, translations = {} } = this._table; - - headers = Object.keys(headers).reduce((result, key) => { +function prepareMoHeaders (headers) { + return Object.keys(headers).reduce((/** @type {{[key: string]: string}} */ result, key) => { const lowerKey = key.toLowerCase(); if (HEADERS.has(lowerKey)) { // POT-Creation-Date is removed in MO (see https://savannah.gnu.org/bugs/?49654) if (lowerKey !== 'pot-creation-date') { - result[HEADERS.get(lowerKey)] = headers[key]; + const value = HEADERS.get(lowerKey); + if (value) { + result[value] = headers[key]; + } } } else { result[key] = headers[key]; @@ -40,12 +38,20 @@ function Compiler (table = {headers: {}, translations: {}}) { return result; }, {}); +} - // filter out empty translations - translations = Object.keys(translations).reduce((result, msgctxt) => { +/** + * Prepare the translation object to be compatible with MO compiler + * @param {import('./types.js').GetTextTranslations['translations']} translations + * @return {import('./types.js').GetTextTranslations['translations']} + */ +function prepareTranslations (translations) { + return Object.keys(translations).reduce((result, msgctxt) => { const context = translations[msgctxt]; - const msgs = Object.keys(context).reduce((result, msgid) => { - const hasTranslation = context[msgid].msgstr.some(item => !!item.length); + const msgs = Object.keys(context).reduce((/** @type {{[key: string]: string}} */ result, msgid) => { + /** @type {import('./types.js').GetTextTranslation[]} */ + const TranslationMsgstr = context[msgid].msgstr; + const hasTranslation = TranslationMsgstr.some(item => !!item.length); if (hasTranslation) { result[msgid] = context[msgid]; @@ -60,21 +66,31 @@ function Compiler (table = {headers: {}, translations: {}}) { return result; }, {}); +} - this._table.translations = translations; - this._table.headers = headers; +/** + * Creates a MO compiler object. + * + * @param {import('./types.js').GetTextTranslations} [table] Translation table as defined in the README + */ +function Compiler (table) { + /** @type {import('./types.js').GetTextTranslations} _table The translation table */ + this._table = { + charset: undefined, + translations: prepareTranslations(table?.translations ?? {}), + headers: prepareMoHeaders(table?.headers ?? {}) + }; this._translations = []; - this._writeFunc = 'writeUInt32LE'; - this._handleCharset(); -} -/** - * Magic bytes for the generated binary data - */ -Compiler.prototype.MAGIC = 0x950412de; + /** + * Magic bytes for the generated binary data + * @type {number} MAGIC file header magic value of mo file + */ + this.MAGIC = 0x950412de; +} /** * Handles header values, replaces or adds (if needed) a charset property @@ -97,15 +113,17 @@ Compiler.prototype._handleCharset = function () { * Generates an array of translation strings * in the form of [{msgid:..., msgstr: ...}] * - * @return {import('./types.js').GetTextTranslation[]} Translation strings array */ Compiler.prototype._generateList = function () { + /** @type {import('./types.js').GetTextTranslation[]} */ const list = []; - list.push({ - msgid: Buffer.alloc(0), - msgstr: encoding.convert(generateHeader(this._table.headers), this._table.charset) - }); + if ('headers' in this._table) { + list.push({ + msgid: Buffer.alloc(0), + msgstr: encoding.convert(generateHeader(this._table.headers), this._table.charset) + }); + } Object.keys(this._table.translations).forEach(msgctxt => { if (typeof this._table.translations[msgctxt] !== 'object') { @@ -148,7 +166,7 @@ Compiler.prototype._generateList = function () { * Calculate buffer size for the final binary object * * @param {import('./types.js').GetTextTranslation[]} list An array of translation strings from _generateList - * @return {Object} Size data of {msgid, msgstr, total} + * @return {{msgid: number, msgstr: number, total: number}} Size data of {msgid, msgstr, total} */ Compiler.prototype._calculateSize = function (list) { let msgidLength = 0; @@ -182,42 +200,43 @@ Compiler.prototype._calculateSize = function (list) { * Generates the binary MO object from the translation list * * @param {import('./types.js').GetTextTranslation[]} list translation list - * @param {Buffer} size Byte size information - * @return {Buffer} Compiled MO object + * @param {{ msgid: number; msgstr: number; total: number }} size Byte size information + * @return {Buffer} Compiled MO object */ Compiler.prototype._build = function (list, size) { + /** @type {Buffer} returnBuffer */ const returnBuffer = Buffer.alloc(size.total); let curPosition = 0; let i; let len; // magic - returnBuffer[this._writeFunc](this.MAGIC, 0); + returnBuffer.writeUInt32LE(this.MAGIC, 0); // revision - returnBuffer[this._writeFunc](0, 4); + returnBuffer.writeUInt32LE(0, 4); // string count - returnBuffer[this._writeFunc](list.length, 8); + returnBuffer.writeUInt32LE(list.length, 8); // original string table offset - returnBuffer[this._writeFunc](28, 12); + returnBuffer.writeUInt32LE(28, 12); // translation string table offset - returnBuffer[this._writeFunc](28 + (4 + 4) * list.length, 16); + returnBuffer.writeUInt32LE(28 + (4 + 4) * list.length, 16); // hash table size - returnBuffer[this._writeFunc](0, 20); + returnBuffer.writeUInt32LE(0, 20); // hash table offset - returnBuffer[this._writeFunc](28 + (4 + 4) * list.length * 2, 24); + returnBuffer.writeUInt32LE(28 + (4 + 4) * list.length * 2, 24); - // Build originals table + // Build original table curPosition = 28 + 2 * (4 + 4) * list.length; for (i = 0, len = list.length; i < len; i++) { list[i].msgid.copy(returnBuffer, curPosition); - returnBuffer[this._writeFunc](list[i].msgid.length, 28 + i * 8); - returnBuffer[this._writeFunc](curPosition, 28 + i * 8 + 4); + returnBuffer.writeUInt32LE(list[i].msgid.length, 28 + i * 8); + returnBuffer.writeUInt32LE(curPosition, 28 + i * 8 + 4); returnBuffer[curPosition + list[i].msgid.length] = 0x00; curPosition += list[i].msgid.length + 1; } @@ -225,8 +244,8 @@ Compiler.prototype._build = function (list, size) { // build translation table for (i = 0, len = list.length; i < len; i++) { list[i].msgstr.copy(returnBuffer, curPosition); - returnBuffer[this._writeFunc](list[i].msgstr.length, 28 + (4 + 4) * list.length + i * 8); - returnBuffer[this._writeFunc](curPosition, 28 + (4 + 4) * list.length + i * 8 + 4); + returnBuffer.writeUInt32LE(list[i].msgstr.length, 28 + (4 + 4) * list.length + i * 8); + returnBuffer.writeUInt32LE(curPosition, 28 + (4 + 4) * list.length + i * 8 + 4); returnBuffer[curPosition + list[i].msgstr.length] = 0x00; curPosition += list[i].msgstr.length + 1; } diff --git a/src/moparser.js b/src/moparser.js index 9618dbe..8545e1c 100644 --- a/src/moparser.js +++ b/src/moparser.js @@ -18,41 +18,30 @@ export default function (buffer, defaultCharset) { * Creates a MO parser object. * * @constructor - * @param {Buffer} fileContents Binary MO object + * @param {Buffer|null} fileContents Binary MO object * @param {String} [defaultCharset] Default charset to use */ function Parser (fileContents, defaultCharset = 'iso-8859-1') { this._fileContents = fileContents; - /** - * Method name for writing int32 values, default littleendian - */ - this._writeFunc = 'writeUInt32LE'; - - /** - * Method name for reading int32 values, default littleendian - */ - this._readFunc = 'readUInt32LE'; - this._charset = defaultCharset; /** * Translation table * * @type {import('./types.js').GetTextTranslations} table Translation object - * @private */ this._table = { charset: this._charset, - headers: undefined, + headers: {}, translations: {} }; -} -/** - * Magic constant to check the endianness of the input file - */ -Parser.prototype.MAGIC = 0x950412de; + /** + * Magic constant to check the endianness of the input file + */ + this.MAGIC = 0x950412de; +} /** * Checks if number values in the input file are in big- or little endian format. @@ -60,12 +49,12 @@ Parser.prototype.MAGIC = 0x950412de; * @return {Boolean} Return true if magic was detected */ Parser.prototype._checkMagick = function () { - if (this._fileContents.readUInt32LE(0) === this.MAGIC) { + if (this._fileContents?.readUInt32LE(0) === this.MAGIC) { this._readFunc = 'readUInt32LE'; this._writeFunc = 'writeUInt32LE'; return true; - } else if (this._fileContents.readUInt32BE(0) === this.MAGIC) { + } else if (this._fileContents?.readUInt32BE(0) === this.MAGIC) { this._readFunc = 'readUInt32BE'; this._writeFunc = 'writeUInt32BE'; @@ -80,44 +69,47 @@ Parser.prototype._checkMagick = function () { * Use the first translation string in the file as the header. */ Parser.prototype._loadTranslationTable = function () { - let offsetOriginals = this._offsetOriginals; - let offsetTranslations = this._offsetTranslations; + let offsetOriginals = this._offsetOriginals || 0; + let offsetTranslations = this._offsetTranslations || 0; let position; let length; let msgid; let msgstr; - for (let i = 0; i < this._total; i++) { - // msgid string - length = this._fileContents[this._readFunc](offsetOriginals); - offsetOriginals += 4; - position = this._fileContents[this._readFunc](offsetOriginals); - offsetOriginals += 4; - msgid = this._fileContents.subarray( - position, - position + length - ); - - // matching msgstr - length = this._fileContents[this._readFunc](offsetTranslations); - offsetTranslations += 4; - position = this._fileContents[this._readFunc](offsetTranslations); - offsetTranslations += 4; - msgstr = this._fileContents.subarray( - position, - position + length - ); - - if (!i && !msgid.toString()) { - this._handleCharset(msgstr); + if (this._total) { + for (let i = 0; i < this._total; i++) { + if (this._fileContents === null) continue; + // msgid string + length = this._fileContents.readUInt32LE(offsetOriginals); + offsetOriginals += 4; + position = this._fileContents.readUInt32LE(offsetOriginals); + offsetOriginals += 4; + msgid = this._fileContents.subarray( + position, + position + length + ); + + // matching msgstr + length = this._fileContents.readUInt32LE(offsetTranslations); + offsetTranslations += 4; + position = this._fileContents.readUInt32LE(offsetTranslations); + offsetTranslations += 4; + msgstr = this._fileContents.subarray( + position, + position + length + ); + + if (!i && !msgid.toString()) { + this._handleCharset(msgstr); + } + + msgid = encoding.convert(msgid, 'utf-8', this._charset) + .toString('utf8'); + msgstr = encoding.convert(msgstr, 'utf-8', this._charset) + .toString('utf8'); + + this._addString(msgid, msgstr); } - - msgid = encoding.convert(msgid, 'utf-8', this._charset) - .toString('utf8'); - msgstr = encoding.convert(msgstr, 'utf-8', this._charset) - .toString('utf8'); - - this._addString(msgid, msgstr); } // dump the file contents object @@ -127,7 +119,7 @@ Parser.prototype._loadTranslationTable = function () { /** * Detects charset for MO strings from the header * - * @param {String} headers Header value + * @param {Buffer} headers Header value */ Parser.prototype._handleCharset = function (headers) { const headersStr = headers.toString(); @@ -137,10 +129,9 @@ Parser.prototype._handleCharset = function (headers) { this._charset = this._table.charset = formatCharset(match[1], this._charset); } - headers = encoding.convert(headers, 'utf-8', this._charset) - .toString('utf8'); + headers = encoding.convert(headers, 'utf-8', this._charset); - this._table.headers = parseHeader(headers); + this._table.headers = parseHeader(headers.toString('utf8')); }; /** @@ -151,20 +142,18 @@ Parser.prototype._handleCharset = function (headers) { */ Parser.prototype._addString = function (msgidRaw, msgstrRaw) { const translation = {}; - let msgctxt; + let msgctxt = ''; let msgidPlural; - let msgid = msgidRaw.split('\u0004'); - if (msgid.length > 1) { - msgctxt = msgid.shift(); + const msgidArray = msgidRaw.split('\u0004'); + if (msgidArray.length > 1) { + msgctxt = msgidArray.shift() || ''; translation.msgctxt = msgctxt; - } else { - msgctxt = ''; } - msgid = msgid.join('\u0004'); + msgidRaw = msgidArray.join('\u0004'); - const parts = msgid.split('\u0000'); - msgid = parts.shift(); + const parts = msgidRaw.split('\u0000'); + const msgid = parts.shift() || ''; translation.msgid = msgid; @@ -173,7 +162,7 @@ Parser.prototype._addString = function (msgidRaw, msgstrRaw) { } const msgstr = msgstrRaw.split('\u0000'); - translation.msgstr = [].concat(msgstr || []); + translation.msgstr = [...msgstr]; if (!this._table.translations[msgctxt]) { this._table.translations[msgctxt] = {}; @@ -188,29 +177,29 @@ Parser.prototype._addString = function (msgidRaw, msgstrRaw) { * @return {import("./types.js").GetTextTranslations | false} Translation table */ Parser.prototype.parse = function () { - if (!this._checkMagick()) { + if (!this._checkMagick() || this._fileContents === null) { return false; } /** - * GetText revision nr, usually 0 - */ - this._revision = this._fileContents[this._readFunc](4); + * GetText revision nr, usually 0 + */ + this._revision = this._fileContents.readUInt32LE(4); /** - * Total count of translated strings - */ - this._total = this._fileContents[this._readFunc](8); + * @type {number} Total count of translated strings + */ + this._total = this._fileContents.readUInt32LE(8) ?? 0; /** - * Offset position for original strings table - */ - this._offsetOriginals = this._fileContents[this._readFunc](12); + * @type {number} Offset position for original strings table + */ + this._offsetOriginals = this._fileContents.readUInt32LE(12); /** - * Offset position for translation strings table - */ - this._offsetTranslations = this._fileContents[this._readFunc](16); + * @type {number} Offset position for translation strings table + */ + this._offsetTranslations = this._fileContents.readUInt32LE(16); // Load translations into this._translationTable this._loadTranslationTable(); diff --git a/src/pocompiler.js b/src/pocompiler.js index f6aafea..2a9c209 100644 --- a/src/pocompiler.js +++ b/src/pocompiler.js @@ -19,49 +19,52 @@ export default function (table, options) { } /** - * Creates a PO compiler object. + * Takes the header object and converts all headers into the lowercase format * - * @constructor - * @param {import('./types.js').GetTextTranslations|{}} table Translation table to be compiled - * @param {import('./types.js').parserOptions|{}} [options] Options + * @param {{ [x: string]: any; }} headersRaw the headers to prepare + * @returns {{ [x: string]: any; }} the headers in the lowercase format */ -function Compiler (table = {}, options = {}) { - this._table = table; - this._options = options; - - this._table.translations = this._table.translations || {}; - - let { headers = {} } = this._table; - - headers = Object.keys(headers).reduce((result, key) => { +export function preparePoHeaders (headersRaw) { + return Object.keys(headersRaw).reduce((/** @type {{ [x: string]: any; }} */ result, key) => { const lowerKey = key.toLowerCase(); + const value = HEADERS.get(lowerKey); - if (HEADERS.has(lowerKey)) { - result[HEADERS.get(lowerKey)] = headers[key]; + if (typeof value === 'string') { + result[value] = headersRaw[key]; } else { - result[key] = headers[key]; + result[key] = headersRaw[key]; } return result; }, {}); +} - this._table.headers = headers; - - if (!('foldLength' in this._options)) { - this._options.foldLength = 76; - } - - if (!('escapeCharacters' in this._options)) { - this._options.escapeCharacters = true; - } - - if (!('sort' in this._options)) { - this._options.sort = false; - } - - if (!('eol' in this._options)) { - this._options.eol = '\n'; - } +/** + * Creates a PO compiler object. + * + * @constructor + * @param {import('./types.js').GetTextTranslations} [table] Translation table to be compiled + * @param {Partial} [options] Options + */ +function Compiler (table, options) { + this._table = table ?? { + headers: {}, + charset: undefined, + translations: {} + }; + this._table.translations = { ...this._table.translations }; + + /** @type {import('./types.js').parserOptions} _options The Options object */ + this._options = { + foldLength: 76, + escapeCharacters: true, + sort: false, + eol: '\n', + ...options + }; + + /** @type {{ [x: string]: any; }} the translation table */ + this._table.headers = preparePoHeaders(this._table.headers ?? {}); this._translations = []; @@ -76,7 +79,9 @@ function Compiler (table = {}, options = {}) { * @return {String} A comment string for the PO file */ Compiler.prototype._drawComments = function (comments) { + /** @var {String[]} lines The comment lines to be returned */ const lines = []; + /** @var {{key: keyof import('./types.js').GetTextComment, prefix: string}} type The comment type */ const types = [{ key: 'translator', prefix: '# ' @@ -94,24 +99,25 @@ Compiler.prototype._drawComments = function (comments) { prefix: '#| ' }]; - types.forEach(type => { - if (!comments[type.key]) { - return; + for (const type of types) { + /** @var {import('./types.js').GetTextComment} value The comment type */ + const value = type.key; + if (value in comments) { + const commentLines = comments[value]; + for (const line of commentLines.split(/\r?\n|\r/)) { + lines.push(`${type.prefix}${line}`); + } } + } - comments[type.key].split(/\r?\n|\r/).forEach(line => { - lines.push(`${type.prefix}${line}`); - }); - }); - - return lines.join(this._options.eol); + return lines.length ? lines.join(this._options.eol) : ''; }; /** * Builds a PO string for a single translation object * * @param {import('./types.js').GetTextTranslation} block Translation object - * @param {import('./types.js').GetTextTranslation|{}} [override] Properties of this object will override `block` properties + * @param {Partial} [override] Properties of this object will override `block` properties * @param {boolean} [obsolete] Block is obsolete and must be commented out * @return {String} Translation string for a single object */ @@ -120,12 +126,16 @@ Compiler.prototype._drawBlock = function (block, override = {}, obsolete = false const msgctxt = override.msgctxt || block.msgctxt; const msgid = override.msgid || block.msgid; const msgidPlural = override.msgid_plural || block.msgid_plural; - const msgstr = [].concat(override.msgstr || block.msgstr); - let comments = override.comments || block.comments; + const msgstrData = override.msgstr || block.msgstr; + const msgstr = Array.isArray(msgstrData) ? [...msgstrData] : [msgstrData]; // add comments - if (comments && (comments = this._drawComments(comments))) { - response.push(comments); + const comments = override.comments || block.comments; + if (comments) { + const drawnComments = this._drawComments(comments); + if (drawnComments) { + response.push(drawnComments); + } } if (msgctxt) { @@ -180,7 +190,7 @@ Compiler.prototype._addPOString = function (key = '', value = '', obsolete = fal eol = eol + '#~ '; } - if (foldLength > 0) { + if (foldLength && foldLength > 0) { lines = foldLine(value, foldLength); } else { // split only on new lines @@ -206,49 +216,52 @@ Compiler.prototype._addPOString = function (key = '', value = '', obsolete = fal * Handles header values, replaces or adds (if needed) a charset property */ Compiler.prototype._handleCharset = function () { - const ct = contentType.parse(this._table.headers['Content-Type'] || 'text/plain'); + if (this._table.headers) { + const ct = contentType.parse(this._table.headers['Content-Type'] || 'text/plain'); - const charset = formatCharset(this._table.charset || ct.parameters.charset || 'utf-8'); + const charset = formatCharset(this._table.charset || ct.parameters.charset || 'utf-8'); - // clean up content-type charset independently using fallback if missing - if (ct.parameters.charset) { - ct.parameters.charset = formatCharset(ct.parameters.charset); - } + // clean up content-type charset independently using fallback if missing + if (ct.parameters.charset) { + ct.parameters.charset = formatCharset(ct.parameters.charset); + } - this._table.charset = charset; - this._table.headers['Content-Type'] = contentType.format(ct); + this._table.charset = charset; + this._table.headers['Content-Type'] = contentType.format(ct); + } }; /** * Flatten and sort translations object * - * @param {Object} section Object to be prepared (translations or obsolete) - * @returns {string[]} Prepared array + * @param {{ [msgctxt: string]: { [msgid: string]: import('./types.js').GetTextTranslation }}} section Object to be prepared (translations or obsolete) + * @returns {import('./types.js').GetTextTranslation[]|undefined} Prepared array */ Compiler.prototype._prepareSection = function (section) { + /** @type {import('./types.js').GetTextTranslation[]} response Prepared array */ let response = []; - Object.keys(section).forEach(msgctxt => { + for (const msgctxt in section) { if (typeof section[msgctxt] !== 'object') { return; } - Object.keys(section[msgctxt]).forEach(msgid => { + for (const msgid of Object.keys(section[msgctxt])) { if (typeof section[msgctxt][msgid] !== 'object') { - return; + continue; } if (msgctxt === '' && msgid === '') { - return; + continue; } response.push(section[msgctxt][msgid]); - }); - }); + } + } const { sort } = this._options; - if (sort !== false) { + if (sort) { if (typeof sort === 'function') { response = response.sort(sort); } else { @@ -266,28 +279,34 @@ Compiler.prototype._prepareSection = function (section) { * @return {Buffer} Compiled a PO object */ Compiler.prototype.compile = function () { + if (!this._table.translations) { + throw new Error('No translations found'); + } + /** @type {import('./types.js').GetTextTranslation} headerBlock */ const headerBlock = (this._table.translations[''] && this._table.translations['']['']) || {}; - let response = []; + /** @type {import('./types.js').GetTextTranslation[]|undefined} translations Prepared array */ const translations = this._prepareSection(this._table.translations); - response = translations.map(r => this._drawBlock(r)); + /** @type {String[]|undefined} response Prepared array */ + let response = translations?.map(t => this._drawBlock(t)); if (typeof this._table.obsolete === 'object') { + /** @type {import('./types.js').GetTextTranslation[]|undefined} obsolete Prepared array */ const obsolete = this._prepareSection(this._table.obsolete); - if (obsolete.length) { - response = response.concat(obsolete.map(r => this._drawBlock(r, {}, true))); + if (obsolete && obsolete.length) { + response = response?.concat(obsolete.map(r => this._drawBlock(r, {}, true))); } } - const { eol } = this._options; + const eol = this._options.eol ?? '\n'; - response.unshift(this._drawBlock(headerBlock, { + response?.unshift(this._drawBlock(headerBlock, { msgstr: generateHeader(this._table.headers) })); if (this._table.charset === 'utf-8' || this._table.charset === 'ascii') { - return Buffer.from(response.join(eol + eol) + eol, 'utf-8'); + return Buffer.from(response?.join(eol + eol) + eol, 'utf-8'); } - return encoding.convert(response.join(eol + eol) + eol, this._table.charset); + return encoding.convert(response?.join(eol + eol) + eol, this._table.charset); }; diff --git a/src/poparser.js b/src/poparser.js index 6b48144..6336e51 100644 --- a/src/poparser.js +++ b/src/poparser.js @@ -3,10 +3,17 @@ import { formatCharset, parseNPluralFromHeadersSafely, parseHeader } from './sha import { Transform } from 'readable-stream'; import util from 'util'; +/** + * Po parser options + * @typedef {{ defaultCharset?: string, validation?: boolean }} Options Po parser options + * + * The single Node object in the PO file + * @typedef {{ key?: string, type?: number, value: string, quote?: string, obsolete?: boolean, comments?: import('./types.js').GetTextComment | undefined }} Node PO node + */ + /** * Parses a PO object into translation table * - * @typedef {{ defaultCharset?: string, validation?: boolean }} Options * @param {string | Buffer} input PO object * @param {Options} [options] Optional options with defaultCharset and validation */ @@ -19,7 +26,6 @@ export function poParse (input, options = {}) { /** * Parses a PO stream, emits translation table in object mode * - * @typedef {{ defaultCharset: string, validation: boolean }} Options * @param {Options} [options] Optional options with defaultCharset and validation * @param {import('readable-stream').TransformOptions} [transformOptions] Optional stream options */ @@ -31,7 +37,6 @@ export function poStream (options = {}, transformOptions = {}) { * Creates a PO parser object. * If a PO object is a string, UTF-8 will be used as the charset * - * @typedef {{ defaultCharset?: string, validation?: boolean }} Options * @constructor * @param {string | Buffer} fileContents PO object * @param {Options} options Options with defaultCharset and validation @@ -40,8 +45,10 @@ function Parser (fileContents, { defaultCharset = 'iso-8859-1', validation = fal this._validation = validation; this._charset = defaultCharset; + /** @type {Node[]} Lexed tokens */ this._lex = []; this._escaped = false; + /** @type {Node} */ this._node = {}; this._state = this.states.none; this._lineNumber = 1; @@ -71,6 +78,7 @@ Parser.prototype.parse = function () { * @param {string | Buffer} buf Header value */ Parser.prototype._handleCharset = function (buf = '') { + /** @type {string} */ const str = buf.toString(); let pos; let headers = ''; @@ -92,6 +100,11 @@ Parser.prototype._handleCharset = function (buf = '') { return this._toString(buf); }; +/** + * Converts buffer to string + * @param {string | Buffer} buf Buffer to convert + * @return {string} Converted string + */ Parser.prototype._toString = function (buf) { return encoding.convert(buf, 'utf-8', this._charset).toString('utf-8'); }; @@ -231,16 +244,17 @@ Parser.prototype._lexer = function (chunk) { /** * Join multi line strings * - * @param {Object} tokens Parsed tokens - * @return {Object} Parsed tokens, with multi line strings joined into one + * @param {Node[]} tokens Parsed tokens + * @return {Node[]} Parsed tokens, with multi line strings joined into one */ Parser.prototype._joinStringValues = function (tokens) { + /** @type {Node[]} */ const response = []; let lastNode; for (let i = 0, len = tokens.length; i < len; i++) { if (lastNode && tokens[i].type === this.types.string && lastNode.type === this.types.string) { - lastNode.value += tokens[i].value; + lastNode.value += tokens[i].value ?? ''; } else if (lastNode && tokens[i].type === this.types.comments && lastNode.type === this.types.comments) { lastNode.value += '\n' + tokens[i].value; } else { @@ -255,15 +269,19 @@ Parser.prototype._joinStringValues = function (tokens) { /** * Parse comments into separate comment blocks * - * @param {Object} tokens Parsed tokens + * @param {Node[]} tokens Parsed tokens */ Parser.prototype._parseComments = function (tokens) { + /** @type {Node[]} */ // parse comments - tokens.forEach(node => { + for (const node of tokens) { if (!node || node.type !== this.types.comments) { - return; + continue; } + /** @type {{ + [key: string]: string[]; + }} */ const comment = { translator: [], extracted: [], @@ -272,9 +290,10 @@ Parser.prototype._parseComments = function (tokens) { previous: [] }; + /** @type {string[]} */ const lines = (node.value || '').split(/\n/); - lines.forEach(line => { + for (const line of lines) { switch (line.charAt(0) || '') { case ':': comment.reference.push(line.substring(1).trim()); @@ -293,27 +312,29 @@ Parser.prototype._parseComments = function (tokens) { default: comment.translator.push(line.replace(/^\s+/, '')); } - }); + } node.value = {}; - Object.keys(comment).forEach(key => { - if (comment[key] && comment[key].length) { + for (const key of Object.keys(comment)) { + if (key && comment[key]?.length) { node.value[key] = comment[key].join('\n'); } - }); - }); + } + } }; /** * Join gettext keys with values * - * @param {Object} tokens Parsed tokens - * @return {Object} Tokens + * @param {Node[]} tokens Parsed tokens + * @return {Node[]} Tokens */ Parser.prototype._handleKeys = function (tokens) { + /** @type {Node[]} */ const response = []; - let lastNode; + /** @type {{ key: string, type?: number, value: string, comments?: string, obsolete?: boolean }} lastNode */ + let lastNode = {}; for (let i = 0, len = tokens.length; i < len; i++) { if (tokens[i].type === this.types.key) { @@ -327,6 +348,7 @@ Parser.prototype._handleKeys = function (tokens) { lastNode.comments = tokens[i - 1].value; } lastNode.value = ''; + /** @type {Node} lastNode */ response.push(lastNode); } else if (tokens[i].type === this.types.string && lastNode) { lastNode.value += tokens[i].value; @@ -339,22 +361,29 @@ Parser.prototype._handleKeys = function (tokens) { /** * Separate different values into individual translation objects * - * @param {Object} tokens Parsed tokens - * @return {Object} Tokens + * @param {Node[]} tokens Parsed tokens + * @return {import("./types.js").GetTextTranslation[]} Tokens */ Parser.prototype._handleValues = function (tokens) { + /** @type {import("./types.js").GetTextTranslation[]} */ const response = []; - let lastNode; + /** @type {{ msgid: string, msgctxt?: string, msgstr: string[], comments?: import('./types.js').GetTextComment, obsolete?: unknown, msgid_plural?: string}} Translation object */ + let lastNode = {}; + /** @type {string | undefined} */ let curContext; + /** @type {import('./types.js').GetTextComment | undefined} */ let curComments; for (let i = 0, len = tokens.length; i < len; i++) { - if (tokens[i].key.toLowerCase() === 'msgctxt') { + const tokenKey = tokens[i].key; + if (!tokenKey) continue; + if (tokenKey.toLowerCase() === 'msgctxt') { curContext = tokens[i].value; curComments = tokens[i].comments; - } else if (tokens[i].key.toLowerCase() === 'msgid') { + } else if (tokenKey.toLowerCase() === 'msgid') { lastNode = { - msgid: tokens[i].value + msgid: tokens[i].value, + msgstr: [] }; if (tokens[i].obsolete) { lastNode.obsolete = true; @@ -372,10 +401,10 @@ Parser.prototype._handleValues = function (tokens) { lastNode.comments = tokens[i].comments; } - curContext = false; - curComments = false; + curContext = undefined; + curComments = undefined; response.push(lastNode); - } else if (tokens[i].key.toLowerCase() === 'msgid_plural') { + } else if (tokenKey.toLowerCase() === 'msgid_plural') { if (lastNode) { if (this._validation && 'msgid_plural' in lastNode) { throw new SyntaxError(`Multiple msgid_plural error: entry "${lastNode.msgid}" in "${lastNode.msgctxt || ''}" context has multiple msgid_plural declarations.`); @@ -388,19 +417,21 @@ Parser.prototype._handleValues = function (tokens) { lastNode.comments = tokens[i].comments; } - curContext = false; - curComments = false; - } else if (tokens[i].key.substring(0, 6).toLowerCase() === 'msgstr') { + curContext = undefined; + curComments = undefined; + } else if (tokenKey.substring(0, 6).toLowerCase() === 'msgstr') { if (lastNode) { - lastNode.msgstr = (lastNode.msgstr || []).concat(tokens[i].value); + const strData = lastNode.msgstr || []; + const tokenValue = tokens[i].value; + lastNode.msgstr = (strData).concat(tokenValue); } if (tokens[i].comments && !lastNode.comments) { lastNode.comments = tokens[i].comments; } - curContext = false; - curComments = false; + curContext = undefined; + curComments = undefined; } } @@ -411,7 +442,7 @@ Parser.prototype._handleValues = function (tokens) { * Validate token * * @param {{ msgid?: string, msgid_plural?: string, msgstr?: string[] }} token Parsed token - * @param {import("./types.js").GetTextTranslation} translations Translation table + * @param {import("./types.js").GetTextTranslations['translations']} translations Translation table * @param {string} msgctxt Message entry context * @param {number} nplurals Number of expected plural forms * @throws {Error} Will throw an error if token validation fails @@ -426,10 +457,6 @@ Parser.prototype._validateToken = function ( msgctxt, nplurals ) { - if (!this._validation) { - return; - } - if (msgid in translations[msgctxt]) { throw new SyntaxError(`Duplicate msgid error: entry "${msgid}" in "${msgctxt}" context has already been declared.`); // eslint-disable-next-line camelcase @@ -445,20 +472,29 @@ Parser.prototype._validateToken = function ( /** * Compose a translation table from tokens object * - * @param {Object} tokens Parsed tokens - * @return {Object} Translation table + * @param {import("./types.js").GetTextTranslation[]} tokens Parsed tokens + * @return {import("./types.js").GetTextTranslations} Translation table */ Parser.prototype._normalize = function (tokens) { + /** + * Translation table to be returned + * @type {{ + * charset: string, + * obsolete?: { [x: string]: { [x: string]: import("./types.js").GetTextTranslation} }, + * headers: import("./types.js").GetTextTranslations['headers'] | undefined, + * translations: import("./types.js").GetTextTranslations['translations'] | {} + * }} table + */ const table = { charset: this._charset, headers: undefined, translations: {} }; let nplurals = 1; - let msgctxt; for (let i = 0, len = tokens.length; i < len; i++) { - msgctxt = tokens[i].msgctxt || ''; + /** @type {string} */ + const msgctxt = tokens[i].msgctxt || ''; if (tokens[i].obsolete) { if (!table.obsolete) { @@ -485,9 +521,13 @@ Parser.prototype._normalize = function (tokens) { nplurals = parseNPluralFromHeadersSafely(table.headers, nplurals); } - this._validateToken(tokens[i], table.translations, msgctxt, nplurals); + if (this._validation) { + this._validateToken(tokens[i], table.translations, msgctxt, nplurals); + } - table.translations[msgctxt][tokens[i].msgid] = tokens[i]; + /** @type {import("./types.js").GetTextTranslation} token */ + const token = tokens[i]; + table.translations[msgctxt][token.msgid] = token; } return table; @@ -496,37 +536,50 @@ Parser.prototype._normalize = function (tokens) { /** * Converts parsed tokens to a translation table * - * @param {Object} tokens Parsed tokens + * @param {Node[]} tokens Parsed tokens * @returns {Object} Translation table */ Parser.prototype._finalize = function (tokens) { + /** + * Translation table + * @type {Node[]} Translation table + */ let data = this._joinStringValues(tokens); this._parseComments(data); + // The PO parser gettext keys with values data = this._handleKeys(data); - data = this._handleValues(data); - return this._normalize(data); + // The PO parser individual translation objects + const dataset = this._handleValues(data); + return this._normalize(dataset); }; /** * Creates a transform stream for parsing PO input + * @constructor + * @inheritDoc {Transform} * + * @private + * @this {PoParserTransform} PoParserTransform * @param {import( "./types.js").parserOptions} options Optional options with defaultCharset and validation - * @param {import('readable-stream').TransformOptions} transformOptions Optional stream options + * @param {import('readable-stream').TransformOptions & {initialTreshold?: number;}} transformOptions Optional stream options */ function PoParserTransform (options, transformOptions) { this.options = options; + /** @type {Parser|false} */ this._parser = false; this._tokens = {}; + /** @type {*[]} */ this._cache = []; this._cacheSize = 0; this.initialTreshold = transformOptions.initialTreshold || 2 * 1024; Transform.call(this, transformOptions); + this._writableState.objectMode = false; this._readableState.objectMode = true; } @@ -536,7 +589,7 @@ util.inherits(PoParserTransform, Transform); * Processes a chunk of the input stream * @param {Buffer} chunk Chunk of the input stream * @param {string} encoding Encoding of the chunk - * @param {Function} done Callback to call when the chunk is processed + * @param {(k?: *)=> void} done Callback to call when the chunk is processed */ PoParserTransform.prototype._transform = function (chunk, encoding, done) { let i; @@ -580,16 +633,16 @@ PoParserTransform.prototype._transform = function (chunk, encoding, done) { } // it seems we found some 8bit bytes from the end of the string, so let's cache these if (len) { - this._cache = [chunk.slice(chunk.length - len)]; + this._cache = [chunk.subarray(chunk.length - len)]; this._cacheSize = this._cache[0].length; - chunk = chunk.slice(0, chunk.length - len); + chunk = chunk.subarray(0, chunk.length - len); } // chunk might be empty if it only continued of 8bit bytes and these were all cached if (chunk.length) { try { this._parser._lexer(this._parser._toString(chunk)); - } catch (error) { + } catch (/** @type {any} error */error) { setImmediate(() => { done(error); }); @@ -616,7 +669,7 @@ PoParserTransform.prototype._flush = function (done) { this._parser = new Parser(chunk, this.options); } - if (chunk) { + if (chunk && this._parser) { try { this._parser._lexer(this._parser._toString(chunk)); } catch (error) { diff --git a/src/shared.js b/src/shared.js index cfe3abe..94811bf 100644 --- a/src/shared.js +++ b/src/shared.js @@ -1,5 +1,7 @@ // see https://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html +/** @type {string} Header name for "Plural-Forms" */ const PLURAL_FORMS = 'Plural-Forms'; +/** @typedef {Map} Headers Map of header keys to header names */ export const HEADERS = new Map([ ['project-id-version', 'Project-Id-Version'], ['report-msgid-bugs-to', 'Report-Msgid-Bugs-To'], @@ -18,12 +20,14 @@ const PLURAL_FORM_HEADER_NPLURALS_REGEX = /nplurals\s*=\s*(?\d+)/; /** * Parses a header string into an object of key-value pairs * - * @param {String} str Header string - * @return {Object} An object of key-value pairs + * @param {string} str Header string + * @return {{[key: string]: string}} An object of key-value pairs */ export function parseHeader (str = '') { - return str.split('\n') - .reduce((headers, line) => { + /** @type {string} Header string */ + return str + .split('\n') + .reduce((/** @type {Record} */ headers, line) => { const parts = line.split(':'); let key = (parts.shift() || '').trim(); @@ -42,7 +46,7 @@ export function parseHeader (str = '') { /** * Attempts to safely parse 'nplurals" value from "Plural-Forms" header * - * @param {Object} [headers = {}] An object with parsed headers + * @param {{[key: string]: string}} [headers] An object with parsed headers * @param {number} fallback Fallback value if "Plural-Forms" header is absent * @returns {number} Parsed result */ @@ -63,8 +67,8 @@ export function parseNPluralFromHeadersSafely (headers, fallback = 1) { /** * Joins a header object of key value pairs into a header string * - * @param {Object} header Object of key value pairs - * @return {String} Header string + * @param {{[key: string]: string}} header Object of key value pairs + * @return {string} An object of key-value pairs */ export function generateHeader (header = {}) { const keys = Object.keys(header) diff --git a/src/types.js b/src/types.js index 269befa..527c225 100644 --- a/src/types.js +++ b/src/types.js @@ -16,14 +16,16 @@ * @property {string} [msgid_plural] The plural message ID. * @property {string[]} msgstr Array of translated strings. * @property {GetTextComment} [comments] Comments associated with the translation. + * @property {GetTextTranslation} [obsolete] Whether the translation is obsolete. */ /** * Represents GetText translations. * @typedef {Object} GetTextTranslations - * @property {string} charset Character set. - * @property {Object.} headers Headers. - * @property {Object.>} translations Translations. + * @property {string|undefined} charset Character set. + * @property {{[key: string]: string}} headers Headers. + * @property {GetTextTranslations['translations']} [obsolete] Obsolete messages. + * @property {Object.<[msgctxt: string], Object.<[msgid: string], GetTextTranslation>>} translations Translations. */ /** @@ -31,4 +33,8 @@ * @typedef {Object} parserOptions * @property {string} [defaultCharset] Default character set. * @property {boolean} [validation] Whether to perform validation. + * @property {number} [foldLength] the fold length. + * @property {boolean} [escapeCharacters] Whether to escape characters. + * @property {boolean} [sort] Whether to sort messages. + * @property {string} [eol] End of line character. */ diff --git a/tsconfig.json b/tsconfig.json index 5dd7090..2186fcd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "allowJs": true, // Check js files for errors - "checkJs": false, + "checkJs": true, // the directory sources are in "rootDir": "src", From 977605c0410771f5240585a0264f4a370439a2d1 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Sat, 11 May 2024 16:47:47 +0200 Subject: [PATCH 06/16] @johnhooks review suggestions Co-authored-by: John Hooks --- src/mocompiler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mocompiler.js b/src/mocompiler.js index e8a54a5..6fc7756 100644 --- a/src/mocompiler.js +++ b/src/mocompiler.js @@ -21,7 +21,7 @@ export default function (table) { * @return {{[key: string]: string}} The prepared header */ function prepareMoHeaders (headers) { - return Object.keys(headers).reduce((/** @type {{[key: string]: string}} */ result, key) => { + return Object.keys(headers).reduce((/** @type {Record} */ result, key) => { const lowerKey = key.toLowerCase(); if (HEADERS.has(lowerKey)) { From 4262c6d539c79f8cb359f1fc55e1aa36c6af3189 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Sat, 11 May 2024 17:02:03 +0200 Subject: [PATCH 07/16] apply suggestion by johnhooks Co-authored-by: John Hooks --- src/moparser.js | 4 ++-- src/pocompiler.js | 6 +++--- src/shared.js | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/moparser.js b/src/moparser.js index 8545e1c..a4400df 100644 --- a/src/moparser.js +++ b/src/moparser.js @@ -137,8 +137,8 @@ Parser.prototype._handleCharset = function (headers) { /** * Adds a translation to the translation object * - * @param {String} msgidRaw Original string - * @param {String} msgstrRaw Translation for the original string + * @param {string} msgidRaw Original string + * @param {string} msgstrRaw Translation for the original string */ Parser.prototype._addString = function (msgidRaw, msgstrRaw) { const translation = {}; diff --git a/src/pocompiler.js b/src/pocompiler.js index 2a9c209..cf22239 100644 --- a/src/pocompiler.js +++ b/src/pocompiler.js @@ -21,8 +21,8 @@ export default function (table, options) { /** * Takes the header object and converts all headers into the lowercase format * - * @param {{ [x: string]: any; }} headersRaw the headers to prepare - * @returns {{ [x: string]: any; }} the headers in the lowercase format + * @param {Record} headersRaw the headers to prepare + * @returns {Record} the headers in the lowercase format */ export function preparePoHeaders (headersRaw) { return Object.keys(headersRaw).reduce((/** @type {{ [x: string]: any; }} */ result, key) => { @@ -287,7 +287,7 @@ Compiler.prototype.compile = function () { /** @type {import('./types.js').GetTextTranslation[]|undefined} translations Prepared array */ const translations = this._prepareSection(this._table.translations); - /** @type {String[]|undefined} response Prepared array */ + /** @type {string[]|undefined} response Prepared array */ let response = translations?.map(t => this._drawBlock(t)); if (typeof this._table.obsolete === 'object') { diff --git a/src/shared.js b/src/shared.js index 94811bf..eb01f84 100644 --- a/src/shared.js +++ b/src/shared.js @@ -87,9 +87,9 @@ export function generateHeader (header = {}) { /** * Normalizes charset name. Converts utf8 to utf-8, WIN1257 to windows-1257 etc. * - * @param {String} charset Charset name - * @param {String} defaultCharset Default charset name, defaults to 'iso-8859-1' - * @return {String} Normalized charset name + * @param {string} charset Charset name + * @param {string} defaultCharset Default charset name, defaults to 'iso-8859-1' + * @return {string} Normalized charset name */ export function formatCharset (charset = 'iso-8859-1', defaultCharset = 'iso-8859-1') { return charset.toString() From 002ae755a8cb051d9223702f53a345b8deb56f97 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Sat, 11 May 2024 18:24:42 +0200 Subject: [PATCH 08/16] =?UTF-8?q?apply=20@johnhooks=20suggestions=20?= =?UTF-8?q?=F0=9F=99=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/mocompiler.js | 21 +++++++----- src/moparser.js | 86 +++++++++++++++++++++++++++-------------------- src/pocompiler.js | 12 ++++--- 3 files changed, 68 insertions(+), 51 deletions(-) diff --git a/src/mocompiler.js b/src/mocompiler.js index 6fc7756..621864a 100644 --- a/src/mocompiler.js +++ b/src/mocompiler.js @@ -2,6 +2,8 @@ import encoding from 'encoding'; import { HEADERS, formatCharset, generateHeader, compareMsgid } from './shared.js'; import contentType from 'content-type'; +/** @typedef {{msgid: number, msgstr: number, total: number}} Size data of {msgid, msgstr, total} */ + /** * Exposes general compiler function. Takes a translation * object as a parameter and returns binary MO object @@ -83,6 +85,8 @@ function Compiler (table) { this._translations = []; + this._writeFunc = 'writeUInt32LE'; + this._handleCharset(); /** @@ -166,7 +170,7 @@ Compiler.prototype._generateList = function () { * Calculate buffer size for the final binary object * * @param {import('./types.js').GetTextTranslation[]} list An array of translation strings from _generateList - * @return {{msgid: number, msgstr: number, total: number}} Size data of {msgid, msgstr, total} + * @return {Size} Size data of {msgid, msgstr, total} */ Compiler.prototype._calculateSize = function (list) { let msgidLength = 0; @@ -204,32 +208,31 @@ Compiler.prototype._calculateSize = function (list) { * @return {Buffer} Compiled MO object */ Compiler.prototype._build = function (list, size) { - /** @type {Buffer} returnBuffer */ const returnBuffer = Buffer.alloc(size.total); let curPosition = 0; let i; let len; // magic - returnBuffer.writeUInt32LE(this.MAGIC, 0); + returnBuffer[this._writeFunc](this.MAGIC, 0); // revision - returnBuffer.writeUInt32LE(0, 4); + returnBuffer[this._writeFunc](0, 4); // string count - returnBuffer.writeUInt32LE(list.length, 8); + returnBuffer[this._writeFunc](list.length, 8); // original string table offset - returnBuffer.writeUInt32LE(28, 12); + returnBuffer[this._writeFunc](28, 12); // translation string table offset - returnBuffer.writeUInt32LE(28 + (4 + 4) * list.length, 16); + returnBuffer[this._writeFunc](28 + (4 + 4) * list.length, 16); // hash table size - returnBuffer.writeUInt32LE(0, 20); + returnBuffer[this._writeFunc](0, 20); // hash table offset - returnBuffer.writeUInt32LE(28 + (4 + 4) * list.length * 2, 24); + returnBuffer[this._writeFunc](28 + (4 + 4) * list.length * 2, 24); // Build original table curPosition = 28 + 2 * (4 + 4) * list.length; diff --git a/src/moparser.js b/src/moparser.js index a4400df..2d23698 100644 --- a/src/moparser.js +++ b/src/moparser.js @@ -26,6 +26,16 @@ function Parser (fileContents, defaultCharset = 'iso-8859-1') { this._charset = defaultCharset; + /** + * Method name for writing int32 values, default littleendian + */ + this._writeFunc = 'writeUInt32LE'; + + /** + * Method name for reading int32 values, default littleendian + */ + this._readFunc = 'readUInt32LE'; + /** * Translation table * @@ -76,40 +86,42 @@ Parser.prototype._loadTranslationTable = function () { let msgid; let msgstr; - if (this._total) { - for (let i = 0; i < this._total; i++) { - if (this._fileContents === null) continue; - // msgid string - length = this._fileContents.readUInt32LE(offsetOriginals); - offsetOriginals += 4; - position = this._fileContents.readUInt32LE(offsetOriginals); - offsetOriginals += 4; - msgid = this._fileContents.subarray( - position, - position + length - ); - - // matching msgstr - length = this._fileContents.readUInt32LE(offsetTranslations); - offsetTranslations += 4; - position = this._fileContents.readUInt32LE(offsetTranslations); - offsetTranslations += 4; - msgstr = this._fileContents.subarray( - position, - position + length - ); - - if (!i && !msgid.toString()) { - this._handleCharset(msgstr); - } - - msgid = encoding.convert(msgid, 'utf-8', this._charset) - .toString('utf8'); - msgstr = encoding.convert(msgstr, 'utf-8', this._charset) - .toString('utf8'); - - this._addString(msgid, msgstr); + // Return if there are no translations + if (!this._total) { this._fileContents = null; return; } + + // Loop through all strings in the MO file + for (let i = 0; i < this._total; i++) { + if (this._fileContents === null) continue; + // msgid string + length = this._fileContents[this._readFunc](offsetOriginals); + offsetOriginals += 4; + position = this._fileContents[this._readFunc](offsetOriginals); + offsetOriginals += 4; + msgid = this._fileContents.subarray( + position, + position + length + ); + + // matching msgstr + length = this._fileContents[this._readFunc](offsetTranslations); + offsetTranslations += 4; + position = this._fileContents[this._readFunc](offsetTranslations); + offsetTranslations += 4; + msgstr = this._fileContents.subarray( + position, + position + length + ); + + if (!i && !msgid.toString()) { + this._handleCharset(msgstr); } + + msgid = encoding.convert(msgid, 'utf-8', this._charset) + .toString('utf8'); + msgstr = encoding.convert(msgstr, 'utf-8', this._charset) + .toString('utf8'); + + this._addString(msgid, msgstr); } // dump the file contents object @@ -184,22 +196,22 @@ Parser.prototype.parse = function () { /** * GetText revision nr, usually 0 */ - this._revision = this._fileContents.readUInt32LE(4); + this._revision = this._fileContents[this._readFunc](4); /** * @type {number} Total count of translated strings */ - this._total = this._fileContents.readUInt32LE(8) ?? 0; + this._total = this._fileContents[this._readFunc](8) ?? 0; /** * @type {number} Offset position for original strings table */ - this._offsetOriginals = this._fileContents.readUInt32LE(12); + this._offsetOriginals = this._fileContents[this._readFunc](12); /** * @type {number} Offset position for translation strings table */ - this._offsetTranslations = this._fileContents.readUInt32LE(16); + this._offsetTranslations = this._fileContents[this._readFunc](16); // Load translations into this._translationTable this._loadTranslationTable(); diff --git a/src/pocompiler.js b/src/pocompiler.js index cf22239..a8fa64e 100644 --- a/src/pocompiler.js +++ b/src/pocompiler.js @@ -102,11 +102,13 @@ Compiler.prototype._drawComments = function (comments) { for (const type of types) { /** @var {import('./types.js').GetTextComment} value The comment type */ const value = type.key; - if (value in comments) { - const commentLines = comments[value]; - for (const line of commentLines.split(/\r?\n|\r/)) { - lines.push(`${type.prefix}${line}`); - } + + // ignore empty comments + if (!(value in comments)) { continue; } + + const commentLines = comments[value]; + for (const line of commentLines.split(/\r?\n|\r/)) { + lines.push(`${type.prefix}${line}`); } } From babf333b3c8b2d617c038ca1b41065cc90f1dc49 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Sat, 11 May 2024 21:18:18 +0200 Subject: [PATCH 09/16] wip types --- src/mocompiler.js | 7 +++++-- src/moparser.js | 6 ++++-- src/pocompiler.js | 4 ++-- src/poparser.js | 14 +++++++++----- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/mocompiler.js b/src/mocompiler.js index 621864a..77c2df4 100644 --- a/src/mocompiler.js +++ b/src/mocompiler.js @@ -51,7 +51,7 @@ function prepareTranslations (translations) { return Object.keys(translations).reduce((result, msgctxt) => { const context = translations[msgctxt]; const msgs = Object.keys(context).reduce((/** @type {{[key: string]: string}} */ result, msgid) => { - /** @type {import('./types.js').GetTextTranslation[]} */ + /** @type {import('./types.js').GetTextTranslation[]} TranslationMsgstr */ const TranslationMsgstr = context[msgid].msgstr; const hasTranslation = TranslationMsgstr.some(item => !!item.length); @@ -84,7 +84,10 @@ function Compiler (table) { }; this._translations = []; - + /** + * @typedef {('writeUInt32LE'|'writeUInt32BE')} WriteFunc Type definition for write functions. + * @type {WriteFunc} + */ this._writeFunc = 'writeUInt32LE'; this._handleCharset(); diff --git a/src/moparser.js b/src/moparser.js index 2d23698..29ad8d1 100644 --- a/src/moparser.js +++ b/src/moparser.js @@ -27,12 +27,14 @@ function Parser (fileContents, defaultCharset = 'iso-8859-1') { this._charset = defaultCharset; /** - * Method name for writing int32 values, default littleendian + * @typedef {('writeUInt32LE'|'writeUInt32BE')} WriteFunc Method name for writing int32 values, default littleendian + * @type {WriteFunc} */ this._writeFunc = 'writeUInt32LE'; /** - * Method name for reading int32 values, default littleendian + * @typedef {('readUInt32LE'|'readUInt32BE')} ReadFunc Method name for reading int32 values, default littleendian + * @type {ReadFunc} */ this._readFunc = 'readUInt32LE'; diff --git a/src/pocompiler.js b/src/pocompiler.js index a8fa64e..3333de0 100644 --- a/src/pocompiler.js +++ b/src/pocompiler.js @@ -100,7 +100,7 @@ Compiler.prototype._drawComments = function (comments) { }]; for (const type of types) { - /** @var {import('./types.js').GetTextComment} value The comment type */ + /** @var {keyof import('./types.js').GetTextComment} value The comment type */ const value = type.key; // ignore empty comments @@ -131,7 +131,7 @@ Compiler.prototype._drawBlock = function (block, override = {}, obsolete = false const msgstrData = override.msgstr || block.msgstr; const msgstr = Array.isArray(msgstrData) ? [...msgstrData] : [msgstrData]; - // add comments + /** @type {import('./types.js').GetTextComment|undefined} */ const comments = override.comments || block.comments; if (comments) { const drawnComments = this._drawComments(comments); diff --git a/src/poparser.js b/src/poparser.js index 6336e51..6eae483 100644 --- a/src/poparser.js +++ b/src/poparser.js @@ -37,7 +37,6 @@ export function poStream (options = {}, transformOptions = {}) { * Creates a PO parser object. * If a PO object is a string, UTF-8 will be used as the charset * - * @constructor * @param {string | Buffer} fileContents PO object * @param {Options} options Options with defaultCharset and validation */ @@ -367,7 +366,7 @@ Parser.prototype._handleKeys = function (tokens) { Parser.prototype._handleValues = function (tokens) { /** @type {import("./types.js").GetTextTranslation[]} */ const response = []; - /** @type {{ msgid: string, msgctxt?: string, msgstr: string[], comments?: import('./types.js').GetTextComment, obsolete?: unknown, msgid_plural?: string}} Translation object */ + /** @type {import("./types.js").GetTextTranslation} Translation object */ let lastNode = {}; /** @type {string | undefined} */ let curContext; @@ -537,7 +536,7 @@ Parser.prototype._normalize = function (tokens) { * Converts parsed tokens to a translation table * * @param {Node[]} tokens Parsed tokens - * @returns {Object} Translation table + * @returns {import("./types.js").GetTextTranslations} Translation table */ Parser.prototype._finalize = function (tokens) { /** @@ -556,15 +555,20 @@ Parser.prototype._finalize = function (tokens) { return this._normalize(dataset); }; +/** + * @typedef {import('stream').Stream.Writable} WritableState + */ /** * Creates a transform stream for parsing PO input * @constructor - * @inheritDoc {Transform} + * @class {Duplex} Node transform stream * * @private - * @this {PoParserTransform} PoParserTransform * @param {import( "./types.js").parserOptions} options Optional options with defaultCharset and validation * @param {import('readable-stream').TransformOptions & {initialTreshold?: number;}} transformOptions Optional stream options + * + * @property {WritableState} [_writableState] Optional stream options + * @property {Mode} [readableState] Optional stream options */ function PoParserTransform (options, transformOptions) { this.options = options; From 7dc97d73e3569289795b530fdab808ad1292a3d4 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Sun, 12 May 2024 01:15:24 +0200 Subject: [PATCH 10/16] fixed types --- src/mocompiler.js | 10 +++++-- src/pocompiler.js | 12 +++++---- src/poparser.js | 69 +++++++++++++++++++++++------------------------ src/shared.js | 14 ++++++++++ src/types.js | 2 +- tsconfig.json | 2 +- 6 files changed, 64 insertions(+), 45 deletions(-) diff --git a/src/mocompiler.js b/src/mocompiler.js index 77c2df4..252ec6b 100644 --- a/src/mocompiler.js +++ b/src/mocompiler.js @@ -1,6 +1,7 @@ import encoding from 'encoding'; import { HEADERS, formatCharset, generateHeader, compareMsgid } from './shared.js'; import contentType from 'content-type'; +import { Transform } from 'node:stream'; /** @typedef {{msgid: number, msgstr: number, total: number}} Size data of {msgid, msgstr, total} */ @@ -72,6 +73,7 @@ function prepareTranslations (translations) { /** * Creates a MO compiler object. + * @this {Compiler & Transform} * * @param {import('./types.js').GetTextTranslations} [table] Translation table as defined in the README */ @@ -240,7 +242,9 @@ Compiler.prototype._build = function (list, size) { // Build original table curPosition = 28 + 2 * (4 + 4) * list.length; for (i = 0, len = list.length; i < len; i++) { - list[i].msgid.copy(returnBuffer, curPosition); + /** @type {Buffer} */ + const msgidLength = list[i].msgid; + msgidLength.copy(returnBuffer, curPosition); returnBuffer.writeUInt32LE(list[i].msgid.length, 28 + i * 8); returnBuffer.writeUInt32LE(curPosition, 28 + i * 8 + 4); returnBuffer[curPosition + list[i].msgid.length] = 0x00; @@ -249,7 +253,9 @@ Compiler.prototype._build = function (list, size) { // build translation table for (i = 0, len = list.length; i < len; i++) { - list[i].msgstr.copy(returnBuffer, curPosition); + /** @type {Buffer} */ + const msgstrLength = list[i].msgstr; + msgstrLength.copy(returnBuffer, curPosition); returnBuffer.writeUInt32LE(list[i].msgstr.length, 28 + (4 + 4) * list.length + i * 8); returnBuffer.writeUInt32LE(curPosition, 28 + (4 + 4) * list.length + i * 8 + 4); returnBuffer[curPosition + list[i].msgstr.length] = 0x00; diff --git a/src/pocompiler.js b/src/pocompiler.js index 3333de0..130ae5f 100644 --- a/src/pocompiler.js +++ b/src/pocompiler.js @@ -75,11 +75,11 @@ function Compiler (table, options) { * Converts a comment object to a comment string. The comment object is * in the form of {translator: '', reference: '', extracted: '', flag: '', previous: ''} * - * @param {import('./types.js').GetTextComment} comments A comments object + * @param {Record} comments A comments object * @return {String} A comment string for the PO file */ Compiler.prototype._drawComments = function (comments) { - /** @var {String[]} lines The comment lines to be returned */ + /** @var {Record[]} lines The comment lines to be returned */ const lines = []; /** @var {{key: keyof import('./types.js').GetTextComment, prefix: string}} type The comment type */ const types = [{ @@ -100,14 +100,16 @@ Compiler.prototype._drawComments = function (comments) { }]; for (const type of types) { - /** @var {keyof import('./types.js').GetTextComment} value The comment type */ + /** @var {string} value The comment type */ const value = type.key; // ignore empty comments if (!(value in comments)) { continue; } - const commentLines = comments[value]; - for (const line of commentLines.split(/\r?\n|\r/)) { + const commentLines = comments[value].split(/\r?\n|\r/); + + // add comment lines to comments Array + for (const line of commentLines) { lines.push(`${type.prefix}${line}`); } } diff --git a/src/poparser.js b/src/poparser.js index 6eae483..228474c 100644 --- a/src/poparser.js +++ b/src/poparser.js @@ -1,5 +1,5 @@ import encoding from 'encoding'; -import { formatCharset, parseNPluralFromHeadersSafely, parseHeader } from './shared.js'; +import { formatCharset, parseHeader, parseNPluralFromHeadersSafely, ParserError } from './shared.js'; import { Transform } from 'readable-stream'; import util from 'util'; @@ -8,7 +8,14 @@ import util from 'util'; * @typedef {{ defaultCharset?: string, validation?: boolean }} Options Po parser options * * The single Node object in the PO file - * @typedef {{ key?: string, type?: number, value: string, quote?: string, obsolete?: boolean, comments?: import('./types.js').GetTextComment | undefined }} Node PO node + * @typedef {{ + * key?: string, + * type?: number, + * value: string, + * quote?: string, + * obsolete?: boolean, + * comments?: import('./types.js').GetTextComment | undefined + * }} Node PO node */ /** @@ -137,11 +144,11 @@ Parser.prototype.symbols = { key: /[\w\-[\]]/, keyNames: /^(?:msgctxt|msgid(?:_plural)?|msgstr(?:\[\d+])?)$/ }; - /** * Token parser. Parsed state can be found from this._lex * * @param {String} chunk String + * @throws {ParserError} Throws a SyntaxError if the value doesn't match the key names. */ Parser.prototype._lexer = function (chunk) { let chr; @@ -224,11 +231,8 @@ Parser.prototype._lexer = function (chunk) { case this.states.key: if (!chr.match(this.symbols.key)) { if (!this._node.value.match(this.symbols.keyNames)) { - const err = new SyntaxError(`Error parsing PO data: Invalid key name "${this._node.value}" at line ${this._lineNumber}. This can be caused by an unescaped quote character in a msgid or msgstr value.`); - - err.lineNumber = this._lineNumber; - - throw err; + /** @type {Record} */ + throw new ParserError(`Error parsing PO data: Invalid key name "${this._node.value}" at line ${this._lineNumber}. This can be caused by an unescaped quote character in a msgid or msgstr value.`, this._lineNumber); } this._state = this.states.none; i--; @@ -271,8 +275,6 @@ Parser.prototype._joinStringValues = function (tokens) { * @param {Node[]} tokens Parsed tokens */ Parser.prototype._parseComments = function (tokens) { - /** @type {Node[]} */ - // parse comments for (const node of tokens) { if (!node || node.type !== this.types.comments) { continue; @@ -326,13 +328,13 @@ Parser.prototype._parseComments = function (tokens) { /** * Join gettext keys with values * - * @param {Node[]} tokens Parsed tokens - * @return {Node[]} Tokens + * @param {Node[]& {key: { value: string } } } tokens - Parsed tokens containing key-value pairs + * @return {Node[]} - An array of Nodes representing joined tokens */ Parser.prototype._handleKeys = function (tokens) { /** @type {Node[]} */ const response = []; - /** @type {{ key: string, type?: number, value: string, comments?: string, obsolete?: boolean }} lastNode */ + /** @type {Node & {key: any, obsolete?: boolean, comments?: string}} */ let lastNode = {}; for (let i = 0, len = tokens.length; i < len; i++) { @@ -347,7 +349,7 @@ Parser.prototype._handleKeys = function (tokens) { lastNode.comments = tokens[i - 1].value; } lastNode.value = ''; - /** @type {Node} lastNode */ + /** @type {Node} */ response.push(lastNode); } else if (tokens[i].type === this.types.string && lastNode) { lastNode.value += tokens[i].value; @@ -364,9 +366,8 @@ Parser.prototype._handleKeys = function (tokens) { * @return {import("./types.js").GetTextTranslation[]} Tokens */ Parser.prototype._handleValues = function (tokens) { - /** @type {import("./types.js").GetTextTranslation[]} */ const response = []; - /** @type {import("./types.js").GetTextTranslation} Translation object */ + /** @type {import("./types.js").GetTextTranslation | {msgid_plural?: string, msgctxt?: string, msgstr?: string[], msgid: string, comments?: import("./types.js").GetTextComment, obsolete?: boolean}} Translation object */ let lastNode = {}; /** @type {string | undefined} */ let curContext; @@ -471,7 +472,7 @@ Parser.prototype._validateToken = function ( /** * Compose a translation table from tokens object * - * @param {import("./types.js").GetTextTranslation[]} tokens Parsed tokens + * @param {import("./types.js").GetTextTranslation[] & Node[]} tokens Parsed tokens * @return {import("./types.js").GetTextTranslations} Translation table */ Parser.prototype._normalize = function (tokens) { @@ -559,17 +560,13 @@ Parser.prototype._finalize = function (tokens) { * @typedef {import('stream').Stream.Writable} WritableState */ /** - * Creates a transform stream for parsing PO input - * @constructor - * @class {Duplex} Node transform stream - * - * @private - * @param {import( "./types.js").parserOptions} options Optional options with defaultCharset and validation - * @param {import('readable-stream').TransformOptions & {initialTreshold?: number;}} transformOptions Optional stream options - * - * @property {WritableState} [_writableState] Optional stream options - * @property {Mode} [readableState] Optional stream options - */ + * Creates a transform stream for parsing PO input + * @constructor + * @this {PoParserTransform & Transform} + * + * @param {import( "./types.js").parserOptions} options Optional options with defaultCharset and validation + * @param {import('readable-stream').TransformOptions & {initialTreshold?: number;}} transformOptions Optional stream options + */ function PoParserTransform (options, transformOptions) { this.options = options; /** @type {Parser|false} */ @@ -590,11 +587,11 @@ function PoParserTransform (options, transformOptions) { util.inherits(PoParserTransform, Transform); /** - * Processes a chunk of the input stream - * @param {Buffer} chunk Chunk of the input stream - * @param {string} encoding Encoding of the chunk - * @param {(k?: *)=> void} done Callback to call when the chunk is processed - */ + * Processes a chunk of the input stream + * @param {Buffer} chunk Chunk of the input stream + * @param {string} encoding Encoding of the chunk + * @param {(k?: *)=> void} done Callback to call when the chunk is processed + */ PoParserTransform.prototype._transform = function (chunk, encoding, done) { let i; let len = 0; @@ -659,9 +656,9 @@ PoParserTransform.prototype._transform = function (chunk, encoding, done) { }; /** - * Once all inputs have been processed, emit the parsed translation table as an object - * @param {Function} done Callback to call when the chunk is processed - */ + * Once all inputs have been processed, emit the parsed translation table as an object + * @param {} done Callback to call when the chunk is processed + */ PoParserTransform.prototype._flush = function (done) { let chunk; diff --git a/src/shared.js b/src/shared.js index eb01f84..35b2d2f 100644 --- a/src/shared.js +++ b/src/shared.js @@ -165,3 +165,17 @@ export function compareMsgid ({ msgid: left }, { msgid: right }) { return 0; } + +/** + * Custom SyntaxError subclass that includes the lineNumber property. + */ +export class ParserError extends SyntaxError { + /** + * @param {string} message - Error message. + * @param {number} lineNumber - Line number where the error occurred. + */ + constructor (message, lineNumber) { + super(message); + this.lineNumber = lineNumber; + } +} diff --git a/src/types.js b/src/types.js index 527c225..9b4440e 100644 --- a/src/types.js +++ b/src/types.js @@ -14,7 +14,7 @@ * @property {string} [msgctxt] Context of the message. * @property {string} msgid The singular message ID. * @property {string} [msgid_plural] The plural message ID. - * @property {string[]} msgstr Array of translated strings. + * @property {string} msgstr Array of translated strings. * @property {GetTextComment} [comments] Comments associated with the translation. * @property {GetTextTranslation} [obsolete] Whether the translation is obsolete. */ diff --git a/tsconfig.json b/tsconfig.json index 2186fcd..5dd7090 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "allowJs": true, // Check js files for errors - "checkJs": true, + "checkJs": false, // the directory sources are in "rootDir": "src", From aad7d48c1e579519cfbabf4a992b31a94f444a55 Mon Sep 17 00:00:00 2001 From: Erik Golinelli Date: Sun, 12 May 2024 02:29:12 +0200 Subject: [PATCH 11/16] allows tsc to fail in ci tests --- .github/workflows/ci.yml | 18 +++++++++++++----- tsconfig.json | 2 +- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 77f0543..d9de309 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,15 +3,24 @@ on: - push - pull_request jobs: + build: + name: Build with tsc + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - run: npm install + - run: npx tsc test: - name: Node.js ${{ matrix.node-version }} on ${{ matrix.os }} + name: Test ${{ matrix.node-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - node-version: - - 18 - - 20 + node: [ 18, 20 ] os: - ubuntu-latest - windows-latest @@ -21,5 +30,4 @@ jobs: with: node-version: ${{ matrix.node-version }} - run: npm install - - run: npx tsc - run: npm test diff --git a/tsconfig.json b/tsconfig.json index 5dd7090..2186fcd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "allowJs": true, // Check js files for errors - "checkJs": false, + "checkJs": true, // the directory sources are in "rootDir": "src", From d242a947fc916c62795bea6b53a6f8c79ab925a0 Mon Sep 17 00:00:00 2001 From: John Hooks Date: Sat, 11 May 2024 22:14:46 -0700 Subject: [PATCH 12/16] fix: adjust typing of the parsers and compilers This commit adds missing types and attempts fix type errors. There are still a few type errors, though how to fix them is not clear. Adds the `Translations` type for the `translations` property of the `GetTextTranslations` type. --- src/mocompiler.js | 62 ++++++++++++++++++++-------------- src/moparser.js | 21 +++++++----- src/pocompiler.js | 56 ++++++++++++++++-------------- src/poparser.js | 86 ++++++++++++++++++++++++----------------------- src/shared.js | 15 +++++---- src/types.js | 24 +++++++++---- 6 files changed, 150 insertions(+), 114 deletions(-) diff --git a/src/mocompiler.js b/src/mocompiler.js index 252ec6b..d509bd0 100644 --- a/src/mocompiler.js +++ b/src/mocompiler.js @@ -1,15 +1,31 @@ import encoding from 'encoding'; import { HEADERS, formatCharset, generateHeader, compareMsgid } from './shared.js'; import contentType from 'content-type'; -import { Transform } from 'node:stream'; -/** @typedef {{msgid: number, msgstr: number, total: number}} Size data of {msgid, msgstr, total} */ +/** + * @typedef {import('node:stream').Transform} Transform + * @typedef {import('./types.js').GetTextTranslation} GetTextTranslation + * @typedef {import('./types.js').GetTextTranslations} GetTextTranslations + * @typedef {import('./types.js').Translations} Translations + * @typedef {import('./types.js').WriteFunc} WriteFunc + */ + +/** + * @typedef {Object} Size Data about the size of the compiled MO object. + * @property {number} msgid The size of the msgid section. + * @property {number} msgstr The size of the msgstr section. + * @property {number} total The total size of the compiled MO object. + */ + +/** + * @typedef {{ msgid: Buffer, msgstr: Buffer }} InProgressTranslation A translation object partially parsed. + */ /** * Exposes general compiler function. Takes a translation * object as a parameter and returns binary MO object * - * @param {import('./types.js').GetTextTranslations} table Translation object + * @param {GetTextTranslations} table Translation object * @return {Buffer} Compiled binary MO object */ export default function (table) { @@ -20,11 +36,11 @@ export default function (table) { /** * Prepare the header object to be compatible with MO compiler - * @param {{[key: string]: string}} headers the headers - * @return {{[key: string]: string}} The prepared header + * @param {Record} headers the headers + * @return {Record} The prepared header */ function prepareMoHeaders (headers) { - return Object.keys(headers).reduce((/** @type {Record} */ result, key) => { + return Object.keys(headers).reduce((result, key) => { const lowerKey = key.toLowerCase(); if (HEADERS.has(lowerKey)) { @@ -40,19 +56,18 @@ function prepareMoHeaders (headers) { } return result; - }, {}); + }, /** @type {Record} */ ({})); } /** * Prepare the translation object to be compatible with MO compiler - * @param {import('./types.js').GetTextTranslations['translations']} translations - * @return {import('./types.js').GetTextTranslations['translations']} + * @param {Translations} translations + * @return {Translations} */ function prepareTranslations (translations) { return Object.keys(translations).reduce((result, msgctxt) => { const context = translations[msgctxt]; - const msgs = Object.keys(context).reduce((/** @type {{[key: string]: string}} */ result, msgid) => { - /** @type {import('./types.js').GetTextTranslation[]} TranslationMsgstr */ + const msgs = Object.keys(context).reduce((result, msgid) => { const TranslationMsgstr = context[msgid].msgstr; const hasTranslation = TranslationMsgstr.some(item => !!item.length); @@ -61,24 +76,24 @@ function prepareTranslations (translations) { } return result; - }, {}); + }, /** @type {Record} */({})); if (Object.keys(msgs).length) { result[msgctxt] = msgs; } return result; - }, {}); + }, /** @type {Translations} */({})); } /** * Creates a MO compiler object. * @this {Compiler & Transform} * - * @param {import('./types.js').GetTextTranslations} [table] Translation table as defined in the README + * @param {GetTextTranslations} [table] Translation table as defined in the README */ function Compiler (table) { - /** @type {import('./types.js').GetTextTranslations} _table The translation table */ + /** @type {GetTextTranslations} _table The translation table */ this._table = { charset: undefined, translations: prepareTranslations(table?.translations ?? {}), @@ -87,7 +102,6 @@ function Compiler (table) { this._translations = []; /** - * @typedef {('writeUInt32LE'|'writeUInt32BE')} WriteFunc Type definition for write functions. * @type {WriteFunc} */ this._writeFunc = 'writeUInt32LE'; @@ -124,7 +138,7 @@ Compiler.prototype._handleCharset = function () { * */ Compiler.prototype._generateList = function () { - /** @type {import('./types.js').GetTextTranslation[]} */ + /** @type {InProgressTranslation[]} */ const list = []; if ('headers' in this._table) { @@ -159,7 +173,7 @@ Compiler.prototype._generateList = function () { key += '\u0000' + msgidPlural; } - const value = [].concat(this._table.translations[msgctxt][msgid].msgstr || []).join('\u0000'); + const value = /** @type {string[]} */([]).concat(this._table.translations[msgctxt][msgid].msgstr ?? []).join('\u0000'); list.push({ msgid: encoding.convert(key, this._table.charset), @@ -174,7 +188,7 @@ Compiler.prototype._generateList = function () { /** * Calculate buffer size for the final binary object * - * @param {import('./types.js').GetTextTranslation[]} list An array of translation strings from _generateList + * @param {InProgressTranslation[]} list An array of translation strings from _generateList * @return {Size} Size data of {msgid, msgstr, total} */ Compiler.prototype._calculateSize = function (list) { @@ -208,8 +222,8 @@ Compiler.prototype._calculateSize = function (list) { /** * Generates the binary MO object from the translation list * - * @param {import('./types.js').GetTextTranslation[]} list translation list - * @param {{ msgid: number; msgstr: number; total: number }} size Byte size information + * @param {GetTextTranslation[]} list translation list + * @param {Size} size Byte size information * @return {Buffer} Compiled MO object */ Compiler.prototype._build = function (list, size) { @@ -242,8 +256,7 @@ Compiler.prototype._build = function (list, size) { // Build original table curPosition = 28 + 2 * (4 + 4) * list.length; for (i = 0, len = list.length; i < len; i++) { - /** @type {Buffer} */ - const msgidLength = list[i].msgid; + const msgidLength = /** @type {Buffer} */(/** @type {unknown} */(list[i].msgid)); msgidLength.copy(returnBuffer, curPosition); returnBuffer.writeUInt32LE(list[i].msgid.length, 28 + i * 8); returnBuffer.writeUInt32LE(curPosition, 28 + i * 8 + 4); @@ -253,8 +266,7 @@ Compiler.prototype._build = function (list, size) { // build translation table for (i = 0, len = list.length; i < len; i++) { - /** @type {Buffer} */ - const msgstrLength = list[i].msgstr; + const msgstrLength = /** @type {Buffer} */(/** @type {unknown} */(list[i].msgstr)); msgstrLength.copy(returnBuffer, curPosition); returnBuffer.writeUInt32LE(list[i].msgstr.length, 28 + (4 + 4) * list.length + i * 8); returnBuffer.writeUInt32LE(curPosition, 28 + (4 + 4) * list.length + i * 8 + 4); diff --git a/src/moparser.js b/src/moparser.js index 29ad8d1..263efdb 100644 --- a/src/moparser.js +++ b/src/moparser.js @@ -1,12 +1,19 @@ import encoding from 'encoding'; import { formatCharset, parseHeader } from './shared.js'; +/** + * @typedef {import('./types.js').GetTextTranslations} GetTextTranslations + * @typedef {import('./types.js').GetTextTranslation} GetTextTranslation + * @typedef {import('./types.js').Translations} Translations + * @typedef {import('./types.js').WriteFunc} WriteFunc + * @typedef {import('./types.js').ReadFunc} ReadFunc + */ + /** * Parses a binary MO object into translation table * * @param {Buffer} buffer Binary MO object - * @param {String} [defaultCharset] Default charset to use - * @return {Object} Translation object + * @param {string} [defaultCharset] Default charset to use */ export default function (buffer, defaultCharset) { const parser = new Parser(buffer, defaultCharset); @@ -19,7 +26,7 @@ export default function (buffer, defaultCharset) { * * @constructor * @param {Buffer|null} fileContents Binary MO object - * @param {String} [defaultCharset] Default charset to use + * @param {string} [defaultCharset] Default charset to use */ function Parser (fileContents, defaultCharset = 'iso-8859-1') { this._fileContents = fileContents; @@ -27,13 +34,11 @@ function Parser (fileContents, defaultCharset = 'iso-8859-1') { this._charset = defaultCharset; /** - * @typedef {('writeUInt32LE'|'writeUInt32BE')} WriteFunc Method name for writing int32 values, default littleendian * @type {WriteFunc} */ this._writeFunc = 'writeUInt32LE'; /** - * @typedef {('readUInt32LE'|'readUInt32BE')} ReadFunc Method name for reading int32 values, default littleendian * @type {ReadFunc} */ this._readFunc = 'readUInt32LE'; @@ -41,7 +46,7 @@ function Parser (fileContents, defaultCharset = 'iso-8859-1') { /** * Translation table * - * @type {import('./types.js').GetTextTranslations} table Translation object + * @type {GetTextTranslations} table Translation object */ this._table = { charset: this._charset, @@ -58,7 +63,7 @@ function Parser (fileContents, defaultCharset = 'iso-8859-1') { /** * Checks if number values in the input file are in big- or little endian format. * - * @return {Boolean} Return true if magic was detected + * @return {boolean} Return true if magic was detected */ Parser.prototype._checkMagick = function () { if (this._fileContents?.readUInt32LE(0) === this.MAGIC) { @@ -188,7 +193,7 @@ Parser.prototype._addString = function (msgidRaw, msgstrRaw) { /** * Parses the MO object and returns translation table * - * @return {import("./types.js").GetTextTranslations | false} Translation table + * @return {GetTextTranslations | false} Translation table */ Parser.prototype.parse = function () { if (!this._checkMagick() || this._fileContents === null) { diff --git a/src/pocompiler.js b/src/pocompiler.js index 130ae5f..2d02266 100644 --- a/src/pocompiler.js +++ b/src/pocompiler.js @@ -4,12 +4,19 @@ import contentType from 'content-type'; // @ts-expect-error TS7016: Could not find a declaration file for module encoding. import encoding from 'encoding'; +/** + * @typedef {import('./types.js').GetTextTranslations} GetTextTranslations + * @typedef {import('./types.js').GetTextTranslation} GetTextTranslation + * @typedef {import('./types.js').GetTextComment} GetTextComment + * @typedef {import('./types.js').Translations} Translations + * @typedef {import('./types.js').ParserOptions} ParserOptions + */ /** * Exposes general compiler function. Takes a translation * object as a parameter and returns PO object * - * @param {import('./types.js').GetTextTranslations} table Translation object - * @param {import('./types.js').parserOptions|{}} [options] Options + * @param {GetTextTranslations} table Translation object + * @param {ParserOptions} [options] Options * @return {Buffer} The compiled PO object */ export default function (table, options) { @@ -21,11 +28,11 @@ export default function (table, options) { /** * Takes the header object and converts all headers into the lowercase format * - * @param {Record} headersRaw the headers to prepare - * @returns {Record} the headers in the lowercase format + * @param {Record} headersRaw the headers to prepare + * @returns {Record} the headers in the lowercase format */ export function preparePoHeaders (headersRaw) { - return Object.keys(headersRaw).reduce((/** @type {{ [x: string]: any; }} */ result, key) => { + return Object.keys(headersRaw).reduce((result, key) => { const lowerKey = key.toLowerCase(); const value = HEADERS.get(lowerKey); @@ -36,15 +43,15 @@ export function preparePoHeaders (headersRaw) { } return result; - }, {}); + }, /** @type {Record} */ ({})); } /** * Creates a PO compiler object. * * @constructor - * @param {import('./types.js').GetTextTranslations} [table] Translation table to be compiled - * @param {Partial} [options] Options + * @param {GetTextTranslations} [table] Translation table to be compiled + * @param {ParserOptions} [options] Options */ function Compiler (table, options) { this._table = table ?? { @@ -54,7 +61,7 @@ function Compiler (table, options) { }; this._table.translations = { ...this._table.translations }; - /** @type {import('./types.js').parserOptions} _options The Options object */ + /** @type {ParserOptions} _options The Options object */ this._options = { foldLength: 76, escapeCharacters: true, @@ -63,7 +70,7 @@ function Compiler (table, options) { ...options }; - /** @type {{ [x: string]: any; }} the translation table */ + /** @type {Record}} the translation table */ this._table.headers = preparePoHeaders(this._table.headers ?? {}); this._translations = []; @@ -76,12 +83,12 @@ function Compiler (table, options) { * in the form of {translator: '', reference: '', extracted: '', flag: '', previous: ''} * * @param {Record} comments A comments object - * @return {String} A comment string for the PO file + * @return {string} A comment string for the PO file */ Compiler.prototype._drawComments = function (comments) { /** @var {Record[]} lines The comment lines to be returned */ const lines = []; - /** @var {{key: keyof import('./types.js').GetTextComment, prefix: string}} type The comment type */ + /** @var {{key: GetTextComment, prefix: string}} type The comment type */ const types = [{ key: 'translator', prefix: '# ' @@ -120,10 +127,10 @@ Compiler.prototype._drawComments = function (comments) { /** * Builds a PO string for a single translation object * - * @param {import('./types.js').GetTextTranslation} block Translation object - * @param {Partial} [override] Properties of this object will override `block` properties + * @param {GetTextTranslation} block Translation object + * @param {Partial} [override] Properties of this object will override `block` properties * @param {boolean} [obsolete] Block is obsolete and must be commented out - * @return {String} Translation string for a single object + * @return {string} Translation string for a single object */ Compiler.prototype._drawBlock = function (block, override = {}, obsolete = false) { const response = []; @@ -133,7 +140,7 @@ Compiler.prototype._drawBlock = function (block, override = {}, obsolete = false const msgstrData = override.msgstr || block.msgstr; const msgstr = Array.isArray(msgstrData) ? [...msgstrData] : [msgstrData]; - /** @type {import('./types.js').GetTextComment|undefined} */ + /** @type {GetTextComment|undefined} */ const comments = override.comments || block.comments; if (comments) { const drawnComments = this._drawComments(comments); @@ -164,10 +171,10 @@ Compiler.prototype._drawBlock = function (block, override = {}, obsolete = false /** * Escapes and joins a key and a value for the PO string * - * @param {String} key Key name - * @param {String} value Key value + * @param {string} key Key name + * @param {string} value Key value * @param {boolean} [obsolete] PO string is obsolete and must be commented out - * @return {String} Joined and escaped key-value pair + * @return {string} Joined and escaped key-value pair */ Compiler.prototype._addPOString = function (key = '', value = '', obsolete = false) { key = key.toString(); @@ -238,11 +245,11 @@ Compiler.prototype._handleCharset = function () { /** * Flatten and sort translations object * - * @param {{ [msgctxt: string]: { [msgid: string]: import('./types.js').GetTextTranslation }}} section Object to be prepared (translations or obsolete) - * @returns {import('./types.js').GetTextTranslation[]|undefined} Prepared array + * @param {Translations} section Object to be prepared (translations or obsolete) + * @returns {GetTextTranslation[]|undefined} Prepared array */ Compiler.prototype._prepareSection = function (section) { - /** @type {import('./types.js').GetTextTranslation[]} response Prepared array */ + /** @type {GetTextTranslation[]} response Prepared array */ let response = []; for (const msgctxt in section) { @@ -286,16 +293,13 @@ Compiler.prototype.compile = function () { if (!this._table.translations) { throw new Error('No translations found'); } - /** @type {import('./types.js').GetTextTranslation} headerBlock */ + /** @type {GetTextTranslation} headerBlock */ const headerBlock = (this._table.translations[''] && this._table.translations['']['']) || {}; - /** @type {import('./types.js').GetTextTranslation[]|undefined} translations Prepared array */ const translations = this._prepareSection(this._table.translations); - /** @type {string[]|undefined} response Prepared array */ let response = translations?.map(t => this._drawBlock(t)); if (typeof this._table.obsolete === 'object') { - /** @type {import('./types.js').GetTextTranslation[]|undefined} obsolete Prepared array */ const obsolete = this._prepareSection(this._table.obsolete); if (obsolete && obsolete.length) { response = response?.concat(obsolete.map(r => this._drawBlock(r, {}, true))); diff --git a/src/poparser.js b/src/poparser.js index 228474c..4b13e9d 100644 --- a/src/poparser.js +++ b/src/poparser.js @@ -4,18 +4,29 @@ import { Transform } from 'readable-stream'; import util from 'util'; /** - * Po parser options + * @typedef {import('stream').Stream.Writable} WritableState + * @typedef {import('./types.js').GetTextTranslations} GetTextTranslations + * @typedef {import('./types.js').GetTextTranslation} GetTextTranslation + * @typedef {import('./types.js').GetTextComment} GetTextComment + * @typedef {import('./types.js').Translations} Translations + */ + +/** * @typedef {{ defaultCharset?: string, validation?: boolean }} Options Po parser options - * - * The single Node object in the PO file - * @typedef {{ - * key?: string, - * type?: number, - * value: string, - * quote?: string, - * obsolete?: boolean, - * comments?: import('./types.js').GetTextComment | undefined - * }} Node PO node + */ + +/** + * @typedef {(...args: any[]) => void} DoneCallback + */ + +/** + * @typedef {Object} Node A single Node object in the PO file + * @property {string} [key] + * @property {number} [type] + * @property {string} value + * @property {string} [quote] + * @property {boolean} [obsolete] + * @property {GetTextComment | undefined} [comments] */ /** @@ -54,7 +65,7 @@ function Parser (fileContents, { defaultCharset = 'iso-8859-1', validation = fal /** @type {Node[]} Lexed tokens */ this._lex = []; this._escaped = false; - /** @type {Node} */ + /** @type {Partial} */ this._node = {}; this._state = this.states.none; this._lineNumber = 1; @@ -147,7 +158,7 @@ Parser.prototype.symbols = { /** * Token parser. Parsed state can be found from this._lex * - * @param {String} chunk String + * @param {string} chunk String * @throws {ParserError} Throws a SyntaxError if the value doesn't match the key names. */ Parser.prototype._lexer = function (chunk) { @@ -169,14 +180,14 @@ Parser.prototype._lexer = function (chunk) { value: '', quote: chr }; - this._lex.push(this._node); + this._lex.push(/** @type {Node} */ (this._node)); this._state = this.states.string; } else if (chr === '#') { this._node = { type: this.types.comments, value: '' }; - this._lex.push(this._node); + this._lex.push(/** @type {Node} */ (this._node)); this._state = this.states.comments; } else if (!chr.match(this.symbols.whitespace)) { this._node = { @@ -186,7 +197,7 @@ Parser.prototype._lexer = function (chunk) { if (this._state === this.states.obsolete) { this._node.obsolete = true; } - this._lex.push(this._node); + this._lex.push(/** @type {Node} */ (this._node)); this._state = this.states.key; } break; @@ -230,8 +241,7 @@ Parser.prototype._lexer = function (chunk) { break; case this.states.key: if (!chr.match(this.symbols.key)) { - if (!this._node.value.match(this.symbols.keyNames)) { - /** @type {Record} */ + if (!this._node.value?.match(this.symbols.keyNames)) { throw new ParserError(`Error parsing PO data: Invalid key name "${this._node.value}" at line ${this._lineNumber}. This can be caused by an unescaped quote character in a msgid or msgstr value.`, this._lineNumber); } this._state = this.states.none; @@ -328,13 +338,13 @@ Parser.prototype._parseComments = function (tokens) { /** * Join gettext keys with values * - * @param {Node[]& {key: { value: string } } } tokens - Parsed tokens containing key-value pairs + * @param {(Node & { value?: string })[]} tokens - Parsed tokens containing key-value pairs * @return {Node[]} - An array of Nodes representing joined tokens */ Parser.prototype._handleKeys = function (tokens) { /** @type {Node[]} */ const response = []; - /** @type {Node & {key: any, obsolete?: boolean, comments?: string}} */ + /** @type {Partial & { comments?: string }} */ let lastNode = {}; for (let i = 0, len = tokens.length; i < len; i++) { @@ -349,8 +359,7 @@ Parser.prototype._handleKeys = function (tokens) { lastNode.comments = tokens[i - 1].value; } lastNode.value = ''; - /** @type {Node} */ - response.push(lastNode); + response.push(/** @type {Node} */ (lastNode)); } else if (tokens[i].type === this.types.string && lastNode) { lastNode.value += tokens[i].value; } @@ -363,11 +372,11 @@ Parser.prototype._handleKeys = function (tokens) { * Separate different values into individual translation objects * * @param {Node[]} tokens Parsed tokens - * @return {import("./types.js").GetTextTranslation[]} Tokens + * @return {GetTextTranslation[]} Tokens */ Parser.prototype._handleValues = function (tokens) { const response = []; - /** @type {import("./types.js").GetTextTranslation | {msgid_plural?: string, msgctxt?: string, msgstr?: string[], msgid: string, comments?: import("./types.js").GetTextComment, obsolete?: boolean}} Translation object */ + /** @type {GetTextTranslation} Translation object */ let lastNode = {}; /** @type {string | undefined} */ let curContext; @@ -472,18 +481,13 @@ Parser.prototype._validateToken = function ( /** * Compose a translation table from tokens object * - * @param {import("./types.js").GetTextTranslation[] & Node[]} tokens Parsed tokens - * @return {import("./types.js").GetTextTranslations} Translation table + * @param {GetTextTranslation[]} tokens Parsed tokens + * @return {GetTextTranslations} Translation table */ Parser.prototype._normalize = function (tokens) { /** * Translation table to be returned - * @type {{ - * charset: string, - * obsolete?: { [x: string]: { [x: string]: import("./types.js").GetTextTranslation} }, - * headers: import("./types.js").GetTextTranslations['headers'] | undefined, - * translations: import("./types.js").GetTextTranslations['translations'] | {} - * }} table + * @type {Omit & Partial> } table */ const table = { charset: this._charset, @@ -525,24 +529,22 @@ Parser.prototype._normalize = function (tokens) { this._validateToken(tokens[i], table.translations, msgctxt, nplurals); } - /** @type {import("./types.js").GetTextTranslation} token */ const token = tokens[i]; table.translations[msgctxt][token.msgid] = token; } - return table; + return /** @type {GetTextTranslations} */ (table); }; /** * Converts parsed tokens to a translation table * * @param {Node[]} tokens Parsed tokens - * @returns {import("./types.js").GetTextTranslations} Translation table + * @returns {GetTextTranslations} Translation table */ Parser.prototype._finalize = function (tokens) { /** * Translation table - * @type {Node[]} Translation table */ let data = this._joinStringValues(tokens); @@ -556,15 +558,12 @@ Parser.prototype._finalize = function (tokens) { return this._normalize(dataset); }; -/** - * @typedef {import('stream').Stream.Writable} WritableState - */ /** * Creates a transform stream for parsing PO input * @constructor * @this {PoParserTransform & Transform} * - * @param {import( "./types.js").parserOptions} options Optional options with defaultCharset and validation + * @param {import( "./types.js").ParserOptions} options Optional options with defaultCharset and validation * @param {import('readable-stream').TransformOptions & {initialTreshold?: number;}} transformOptions Optional stream options */ function PoParserTransform (options, transformOptions) { @@ -588,9 +587,10 @@ util.inherits(PoParserTransform, Transform); /** * Processes a chunk of the input stream + * @template {(...args: any[]) => void} DoneCallback * @param {Buffer} chunk Chunk of the input stream * @param {string} encoding Encoding of the chunk - * @param {(k?: *)=> void} done Callback to call when the chunk is processed + * @param {DoneCallback} done Callback to call when the chunk is processed */ PoParserTransform.prototype._transform = function (chunk, encoding, done) { let i; @@ -657,7 +657,9 @@ PoParserTransform.prototype._transform = function (chunk, encoding, done) { /** * Once all inputs have been processed, emit the parsed translation table as an object - * @param {} done Callback to call when the chunk is processed + * + * @template {(...args: any[]) => void} DoneCallback + * @param {DoneCallback} done Callback to call when the chunk is processed */ PoParserTransform.prototype._flush = function (done) { let chunk; diff --git a/src/shared.js b/src/shared.js index 35b2d2f..97806ac 100644 --- a/src/shared.js +++ b/src/shared.js @@ -21,7 +21,7 @@ const PLURAL_FORM_HEADER_NPLURALS_REGEX = /nplurals\s*=\s*(?\d+)/; * Parses a header string into an object of key-value pairs * * @param {string} str Header string - * @return {{[key: string]: string}} An object of key-value pairs + * @return {Record} An object of key-value pairs */ export function parseHeader (str = '') { /** @type {string} Header string */ @@ -46,7 +46,7 @@ export function parseHeader (str = '') { /** * Attempts to safely parse 'nplurals" value from "Plural-Forms" header * - * @param {{[key: string]: string}} [headers] An object with parsed headers + * @param {Record} [headers] An object with parsed headers * @param {number} fallback Fallback value if "Plural-Forms" header is absent * @returns {number} Parsed result */ @@ -67,7 +67,7 @@ export function parseNPluralFromHeadersSafely (headers, fallback = 1) { /** * Joins a header object of key value pairs into a header string * - * @param {{[key: string]: string}} header Object of key value pairs + * @param {Record} header Object of key value pairs * @return {string} An object of key-value pairs */ export function generateHeader (header = {}) { @@ -105,8 +105,8 @@ export function formatCharset (charset = 'iso-8859-1', defaultCharset = 'iso-885 /** * Folds long lines according to PO format * - * @param {String} str PO formatted string to be folded - * @param {Number} [maxLen=76] Maximum allowed length for folded lines + * @param {string} str PO formatted string to be folded + * @param {number} [maxLen=76] Maximum allowed length for folded lines * @return {string[]} An array of lines */ export function foldLine (str, maxLen = 76) { @@ -150,8 +150,9 @@ export function foldLine (str, maxLen = 76) { /** * Comparator function for comparing msgid * - * @param {{msgid: string}} left with msgid prev - * @param {{msgid: string}} right with msgid next + * @template {Buffer|string} T + * @param {{msgid: T}} left with msgid prev + * @param {{msgid: T}} right with msgid next * @returns {number} comparator index */ export function compareMsgid ({ msgid: left }, { msgid: right }) { diff --git a/src/types.js b/src/types.js index 9b4440e..450d778 100644 --- a/src/types.js +++ b/src/types.js @@ -14,23 +14,27 @@ * @property {string} [msgctxt] Context of the message. * @property {string} msgid The singular message ID. * @property {string} [msgid_plural] The plural message ID. - * @property {string} msgstr Array of translated strings. + * @property {string[]} msgstr Array of translated strings. * @property {GetTextComment} [comments] Comments associated with the translation. - * @property {GetTextTranslation} [obsolete] Whether the translation is obsolete. + * @property {boolean} [obsolete] Whether the translation is obsolete. + */ + +/** + * @typedef {Record>} Translations The translations index. */ /** * Represents GetText translations. * @typedef {Object} GetTextTranslations * @property {string|undefined} charset Character set. - * @property {{[key: string]: string}} headers Headers. - * @property {GetTextTranslations['translations']} [obsolete] Obsolete messages. - * @property {Object.<[msgctxt: string], Object.<[msgid: string], GetTextTranslation>>} translations Translations. + * @property {Record} headers Headers. + * @property {Translations} [obsolete] Obsolete messages. + * @property {Translations} translations Translations. */ /** * Options for the parser. - * @typedef {Object} parserOptions + * @typedef {Object} ParserOptions * @property {string} [defaultCharset] Default character set. * @property {boolean} [validation] Whether to perform validation. * @property {number} [foldLength] the fold length. @@ -38,3 +42,11 @@ * @property {boolean} [sort] Whether to sort messages. * @property {string} [eol] End of line character. */ + +/** + * @typedef {('writeUInt32LE'|'writeUInt32BE')} WriteFunc Type definition for write functions. + */ + +/** + * @typedef {('readUInt32LE'|'readUInt32BE')} ReadFunc Type definition for read functions. + */ From 6b3b940741e46564e199078876ed60fc4ea9747b Mon Sep 17 00:00:00 2001 From: John Hooks Date: Sat, 11 May 2024 22:35:03 -0700 Subject: [PATCH 13/16] add imports for types --- src/poparser.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/poparser.js b/src/poparser.js index 4b13e9d..1bde83b 100644 --- a/src/poparser.js +++ b/src/poparser.js @@ -5,10 +5,12 @@ import util from 'util'; /** * @typedef {import('stream').Stream.Writable} WritableState + * @typedef {import('stream').TransformOptions} TransformOptions * @typedef {import('./types.js').GetTextTranslations} GetTextTranslations * @typedef {import('./types.js').GetTextTranslation} GetTextTranslation * @typedef {import('./types.js').GetTextComment} GetTextComment * @typedef {import('./types.js').Translations} Translations + * @typedef {import('./types.js').ParserOptions} ParserOptions */ /** @@ -45,7 +47,7 @@ export function poParse (input, options = {}) { * Parses a PO stream, emits translation table in object mode * * @param {Options} [options] Optional options with defaultCharset and validation - * @param {import('readable-stream').TransformOptions} [transformOptions] Optional stream options + * @param {TransformOptions} [transformOptions] Optional stream options */ export function poStream (options = {}, transformOptions = {}) { return new PoParserTransform(options, transformOptions); @@ -380,7 +382,7 @@ Parser.prototype._handleValues = function (tokens) { let lastNode = {}; /** @type {string | undefined} */ let curContext; - /** @type {import('./types.js').GetTextComment | undefined} */ + /** @type {GetTextComment | undefined} */ let curComments; for (let i = 0, len = tokens.length; i < len; i++) { @@ -450,8 +452,8 @@ Parser.prototype._handleValues = function (tokens) { /** * Validate token * - * @param {{ msgid?: string, msgid_plural?: string, msgstr?: string[] }} token Parsed token - * @param {import("./types.js").GetTextTranslations['translations']} translations Translation table + * @param {GetTextTranslation} token Parsed token + * @param {Translations} translations Translation table * @param {string} msgctxt Message entry context * @param {number} nplurals Number of expected plural forms * @throws {Error} Will throw an error if token validation fails @@ -563,8 +565,8 @@ Parser.prototype._finalize = function (tokens) { * @constructor * @this {PoParserTransform & Transform} * - * @param {import( "./types.js").ParserOptions} options Optional options with defaultCharset and validation - * @param {import('readable-stream').TransformOptions & {initialTreshold?: number;}} transformOptions Optional stream options + * @param {ParserOptions} options Optional options with defaultCharset and validation + * @param {TransformOptions & {initialTreshold?: number;}} transformOptions Optional stream options */ function PoParserTransform (options, transformOptions) { this.options = options; @@ -587,7 +589,6 @@ util.inherits(PoParserTransform, Transform); /** * Processes a chunk of the input stream - * @template {(...args: any[]) => void} DoneCallback * @param {Buffer} chunk Chunk of the input stream * @param {string} encoding Encoding of the chunk * @param {DoneCallback} done Callback to call when the chunk is processed @@ -658,7 +659,6 @@ PoParserTransform.prototype._transform = function (chunk, encoding, done) { /** * Once all inputs have been processed, emit the parsed translation table as an object * - * @template {(...args: any[]) => void} DoneCallback * @param {DoneCallback} done Callback to call when the chunk is processed */ PoParserTransform.prototype._flush = function (done) { From c072f28b0f296481ac9790e8c6d1f404d90f04e4 Mon Sep 17 00:00:00 2001 From: John Hooks Date: Sun, 12 May 2024 06:48:51 -0700 Subject: [PATCH 14/16] add encoding declaration --- .gitignore | 1 + src/mocompiler.js | 8 ++++---- src/pocompiler.js | 16 ++++++++++------ src/poparser.js | 29 ++++++++++++++++------------- tsconfig.json | 20 ++++++++++---------- 5 files changed, 41 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index ee8dea8..da65656 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /node_modules /lib /@types +/types npm-debug.log .DS_Store diff --git a/src/mocompiler.js b/src/mocompiler.js index d509bd0..f538a6d 100644 --- a/src/mocompiler.js +++ b/src/mocompiler.js @@ -18,7 +18,7 @@ import contentType from 'content-type'; */ /** - * @typedef {{ msgid: Buffer, msgstr: Buffer }} InProgressTranslation A translation object partially parsed. + * @typedef {{ msgid: Buffer, msgstr: Buffer }} TranslationBuffers A translation object partially parsed. */ /** @@ -138,7 +138,7 @@ Compiler.prototype._handleCharset = function () { * */ Compiler.prototype._generateList = function () { - /** @type {InProgressTranslation[]} */ + /** @type {TranslationBuffers[]} */ const list = []; if ('headers' in this._table) { @@ -188,7 +188,7 @@ Compiler.prototype._generateList = function () { /** * Calculate buffer size for the final binary object * - * @param {InProgressTranslation[]} list An array of translation strings from _generateList + * @param {TranslationBuffers[]} list An array of translation strings from _generateList * @return {Size} Size data of {msgid, msgstr, total} */ Compiler.prototype._calculateSize = function (list) { @@ -222,7 +222,7 @@ Compiler.prototype._calculateSize = function (list) { /** * Generates the binary MO object from the translation list * - * @param {GetTextTranslation[]} list translation list + * @param {TranslationBuffers[]} list translation list * @param {Size} size Byte size information * @return {Buffer} Compiled MO object */ diff --git a/src/pocompiler.js b/src/pocompiler.js index 2d02266..b107e88 100644 --- a/src/pocompiler.js +++ b/src/pocompiler.js @@ -1,7 +1,6 @@ import { HEADERS, foldLine, compareMsgid, formatCharset, generateHeader } from './shared.js'; import contentType from 'content-type'; -// @ts-expect-error TS7016: Could not find a declaration file for module encoding. import encoding from 'encoding'; /** @@ -11,6 +10,11 @@ import encoding from 'encoding'; * @typedef {import('./types.js').Translations} Translations * @typedef {import('./types.js').ParserOptions} ParserOptions */ + +/** + * @typedef {Partial> & { msgstr?: string | string[] }} PreOutputTranslation + */ + /** * Exposes general compiler function. Takes a translation * object as a parameter and returns PO object @@ -127,8 +131,8 @@ Compiler.prototype._drawComments = function (comments) { /** * Builds a PO string for a single translation object * - * @param {GetTextTranslation} block Translation object - * @param {Partial} [override] Properties of this object will override `block` properties + * @param {PreOutputTranslation} block Translation object + * @param {Partial} [override] Properties of this object will override `block` properties * @param {boolean} [obsolete] Block is obsolete and must be commented out * @return {string} Translation string for a single object */ @@ -246,7 +250,7 @@ Compiler.prototype._handleCharset = function () { * Flatten and sort translations object * * @param {Translations} section Object to be prepared (translations or obsolete) - * @returns {GetTextTranslation[]|undefined} Prepared array + * @returns {PreOutputTranslation[]|undefined} Prepared array */ Compiler.prototype._prepareSection = function (section) { /** @type {GetTextTranslation[]} response Prepared array */ @@ -293,11 +297,11 @@ Compiler.prototype.compile = function () { if (!this._table.translations) { throw new Error('No translations found'); } - /** @type {GetTextTranslation} headerBlock */ + /** @type {PreOutputTranslation} headerBlock */ const headerBlock = (this._table.translations[''] && this._table.translations['']['']) || {}; const translations = this._prepareSection(this._table.translations); - let response = translations?.map(t => this._drawBlock(t)); + let response = /** @type {(PreOutputTranslation|string)[]} */ (/** @type {unknown[]} */ (translations?.map(t => this._drawBlock(t)))); if (typeof this._table.obsolete === 'object') { const obsolete = this._prepareSection(this._table.obsolete); diff --git a/src/poparser.js b/src/poparser.js index 1bde83b..3d209b7 100644 --- a/src/poparser.js +++ b/src/poparser.js @@ -5,7 +5,7 @@ import util from 'util'; /** * @typedef {import('stream').Stream.Writable} WritableState - * @typedef {import('stream').TransformOptions} TransformOptions + * @typedef {import('readable-stream').TransformOptions} TransformOptions * @typedef {import('./types.js').GetTextTranslations} GetTextTranslations * @typedef {import('./types.js').GetTextTranslation} GetTextTranslation * @typedef {import('./types.js').GetTextComment} GetTextComment @@ -327,11 +327,13 @@ Parser.prototype._parseComments = function (tokens) { } } - node.value = {}; + const finalToken = /** @type {Omit & { value: Record}} */ (/** @type {unknown} */ (node)); + + finalToken.value = {}; for (const key of Object.keys(comment)) { if (key && comment[key]?.length) { - node.value[key] = comment[key].join('\n'); + finalToken.value[key] = comment[key].join('\n'); } } } @@ -561,26 +563,27 @@ Parser.prototype._finalize = function (tokens) { }; /** - * Creates a transform stream for parsing PO input - * @constructor - * @this {PoParserTransform & Transform} - * - * @param {ParserOptions} options Optional options with defaultCharset and validation - * @param {TransformOptions & {initialTreshold?: number;}} transformOptions Optional stream options - */ + * Creates a transform stream for parsing PO input + * @constructor + * @this {PoParserTransform & Transform} + * + * @param {ParserOptions} options Optional options with defaultCharset and validation + * @param {TransformOptions & {initialTreshold?: number;}} transformOptions Optional stream options + */ function PoParserTransform (options, transformOptions) { + const { initialTreshold, ..._transformOptions } = transformOptions; this.options = options; /** @type {Parser|false} */ this._parser = false; this._tokens = {}; - /** @type {*[]} */ + /** @type {Buffer[]} */ this._cache = []; this._cacheSize = 0; this.initialTreshold = transformOptions.initialTreshold || 2 * 1024; - Transform.call(this, transformOptions); + Transform.call(this, _transformOptions); this._writableState.objectMode = false; this._readableState.objectMode = true; @@ -685,7 +688,7 @@ PoParserTransform.prototype._flush = function (done) { } if (this._parser) { - this.push(this._parser._finalize(this._parser._lex)); + /** @type {any} */ (this).push(this._parser._finalize(this._parser._lex)); } setImmediate(done); diff --git a/tsconfig.json b/tsconfig.json index 2186fcd..fcc609e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,31 +4,31 @@ "removeComments": false, "module": "Node16", "moduleResolution": "Node16", - "target": "ES2015", - + "target": "ES2018", + "lib": [ + "ES2018" + ], // Strict mode "strict": true, - // Allow javascript files "allowJs": true, - // Check js files for errors "checkJs": true, - // the directory sources are in "rootDir": "src", - // Output d.ts files to @types "outDir": "lib", - // Generate d.ts files "declaration": true, - // Minify "pretty": false, - // Skip lib check when compiling - "skipLibCheck": true + "skipLibCheck": true, + // For providing missing package types + "typeRoots": [ + "./types", + "./node_modules/@types" + ], }, "include": [ "src/**/*", From 9cf82f243b6f11617e2593024d153f34ae6735a3 Mon Sep 17 00:00:00 2001 From: John Hooks Date: Sun, 12 May 2024 06:55:40 -0700 Subject: [PATCH 15/16] add types directory to tsconfig include --- tsconfig.json | 1 + 1 file changed, 1 insertion(+) diff --git a/tsconfig.json b/tsconfig.json index fcc609e..b82537e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -32,5 +32,6 @@ }, "include": [ "src/**/*", + "types/**/*" ] } From b78e19e79a992c5b2dc10e0f3b088758c88140c8 Mon Sep 17 00:00:00 2001 From: John Hooks Date: Sun, 12 May 2024 06:59:03 -0700 Subject: [PATCH 16/16] remove types directory from .gitignore --- .gitignore | 1 - types/encoding/index.d.ts | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 types/encoding/index.d.ts diff --git a/.gitignore b/.gitignore index da65656..ee8dea8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ /node_modules /lib /@types -/types npm-debug.log .DS_Store diff --git a/types/encoding/index.d.ts b/types/encoding/index.d.ts new file mode 100644 index 0000000..3150d35 --- /dev/null +++ b/types/encoding/index.d.ts @@ -0,0 +1,3 @@ +declare module 'encoding' { + function convert(buffer: Buffer | string, charset?: string, fromCharset?: string): Buffer; +}