gctGenerator/scripts/inject_codes.js

383 lines
14 KiB
JavaScript
Raw Normal View History

2020-07-01 14:26:48 +09:00
const fs = require('fs');
const path = require('path');
2020-07-01 15:14:09 +09:00
const { JSDOM } = require('jsdom');
2020-07-03 06:55:48 +09:00
const vuepressContainerPlugin = require('vuepress-plugin-container');
2020-07-01 13:55:31 +09:00
2020-07-03 06:55:48 +09:00
// These plugins have to match the ones used as extensions in .vuepress/config.js
const md = require('@vuepress/markdown')({
2022-02-07 22:18:31 +09:00
plugins: ['@sup39/markdown-it-attr', '@sup39/markdown-it-inline-tag'],
2020-07-03 06:55:48 +09:00
});
2020-07-03 08:21:52 +09:00
const themePlugins = require(path.join(__dirname, '../site/.vuepress/data/themePlugins.json'));
2020-07-03 10:42:43 +09:00
const locales = require(path.join(__dirname, '../site/.vuepress/i18n/locales.json'));
const codeCategories = require(path.join(__dirname, '../site/.vuepress/data/codeCategories.json'));
const presetCategories = require(path.join(
__dirname,
'../site/.vuepress/data/presetCategories.json',
));
2020-07-10 09:39:45 +09:00
const xml = fs.readFileSync(path.join(__dirname, `../Codes.xml`));
2020-07-03 06:55:48 +09:00
// Constants
const JSON_FILE_PATH = path.join(__dirname, '../site/.vuepress/data/gameVersions.json');
const CODE_VERSIONS = ['GMSE01', 'GMSJ01', 'GMSP01', 'GMSJ0A'];
const INJECTION_TAG = '<!-- injectionpoint -->';
2020-07-10 10:29:32 +09:00
/**
* Validates the xml contains usable data
* @param {*} xmlString The xml string
*/
const validateXML = (xmlString) => {
console.log('Validating XML file');
const codeCollection = new JSDOM(xmlString, {
contentType: 'text/xml',
}).window.document.getElementsByTagName('code');
const codes = [...codeCollection];
const localeIdentifiers = Object.keys(locales).map((l) => locales[l].lang);
for (let i = 0; i < codes.length; i++) {
// Fallback title exists
const codeTitle = codes[i].querySelector("title[lang='en-US']");
if (!codeTitle || !codeTitle.textContent)
throw new Error(`Missing Fallback Title (en-US) in code nr ${i}`);
// Code has a valid category
const codeCategory = codes[i].querySelector('category');
if (!codeCategory || !codeCategory.textContent)
throw new Error(`Missing code category in ${codeTitle.textContent}`);
if (!['lib', ...codeCategories.map((c) => c.identifier)].includes(codeCategory.textContent))
throw new Error(`Invalid code category for ${codeTitle.textContent}`);
const codePresets = codes[i].querySelector('presets');
if (codePresets && codePresets.textContent) {
for (const preset of codePresets.textContent.split(',')) {
if (!presetCategories.map((c) => c.identifier).includes(preset))
throw new Error(`Invalid preset ${preset} for ${codeTitle.textContent}`);
}
}
2020-07-10 10:29:32 +09:00
// All lang attributes on all titles are valid
const codeTitles = codes[i].querySelectorAll('title');
for (let j = 0; j < codeTitles.length; j++) {
if (
!codeTitles[j].getAttribute('lang') ||
!localeIdentifiers.includes(codeTitles[j].getAttribute('lang'))
)
throw new Error(`Invalid lang attribute on title ${codeTitles[j].textContent}`);
}
// Fallback description exists
const codeDescription = codes[i].querySelector("description[lang='en-US']");
if (!codeDescription || !codeDescription.textContent)
throw new Error(`Missing Fallback Description (en-US) in code nr ${i}`);
// All lang attributes on all descriptions are valid
const codeDescriptions = codes[i].querySelectorAll('title');
for (let j = 0; j < codeDescriptions.length; j++) {
if (
!codeDescriptions[j].getAttribute('lang') ||
!localeIdentifiers.includes(codeDescriptions[j].getAttribute('lang'))
)
throw new Error(`Invalid lang attribute on description ${codeDescriptions[j].textContent}`);
}
// Version tag exists
if (codes[i].querySelectorAll('version').length !== 1)
throw new Error(`Missing or duplicate version in code '${codeTitle.textContent}'`);
// Author tag exists
if (codes[i].querySelectorAll('author').length !== 1)
throw new Error(`Missing author in code '${codeTitle.textContent}'`);
// At least one source exists
const codeSources = codes[i].querySelectorAll('source');
if (codeSources.length === 0) throw new Error(`No codes for ${codeTitle.textContent}`);
// All sources have a valid version attribute
for (let j = 0; j < codeSources.length; j++) {
if (
!codeSources[j].getAttribute('version') ||
!CODE_VERSIONS.includes(codeSources[j].getAttribute('version'))
)
throw new Error(`Invalid source version for code '${codeTitle.textContent}' at index ${j}`);
}
// Each source has a valid length
for (let j = 0; j < codeSources.length; j++) {
if (
codeSources[j].textContent.replace(/[\s\n\r\t]+/gm, '').length % 16 != 0 ||
codeSources[j].textContent.replace(/[\s\n\r\t]+/gm, '').length < 16
)
throw new Error(
`Invalid source length for code '${codeTitle.textContent}' and version ${
codeSources[j].getAttribute('version').textContent
}`,
);
}
// All titles and descriptions a unique valid lang attribute
for (let j = 0; j < localeIdentifiers.length; j++) {
if (codes[i].querySelectorAll(`title[lang='${localeIdentifiers[j]}']`).length === 0) {
console.warn(
`Missing title translation for code '${codeTitle.textContent}' and locale ${localeIdentifiers[j]}`,
);
2020-08-16 14:42:37 +09:00
console.log(
`::warning file=inject_codes.js::Missing title translation for code '${codeTitle.textContent}' and locale ${localeIdentifiers[j]}`,
2020-08-16 14:22:48 +09:00
);
2020-07-10 10:29:32 +09:00
}
if (codes[i].querySelectorAll(`title[lang='${localeIdentifiers[j]}']`).length > 1) {
throw new Error(
`Duplicate title translation for code '${codeTitle.textContent}' and locale ${localeIdentifiers[j]}`,
);
}
if (codes[i].querySelectorAll(`description[lang='${localeIdentifiers[j]}']`).length === 0) {
console.warn(
`Missing description translation for code '${codeTitle.textContent}' and locale ${localeIdentifiers[j]}`,
);
2020-08-16 14:42:37 +09:00
console.log(
`::warning file=inject_codes.js::Missing description translation for code '${codeTitle.textContent}' and locale ${localeIdentifiers[j]}`,
2020-08-16 14:22:48 +09:00
);
2020-07-10 10:29:32 +09:00
}
if (codes[i].querySelectorAll(`description[lang='${localeIdentifiers[j]}']`).length > 1) {
throw new Error(
`Duplicate description translation for code '${codeTitle.textContent}' and locale ${localeIdentifiers[j]}`,
);
}
}
// All sources have a unique valid version attribute
for (let j = 0; j < CODE_VERSIONS.length; j++) {
2020-08-16 14:22:48 +09:00
if (codes[i].querySelectorAll(`source[version='${CODE_VERSIONS[j]}']`).length === 0) {
2020-07-10 10:29:32 +09:00
console.warn(
`Missing source on code '${codeTitle.textContent}' for version ${CODE_VERSIONS[j]}`,
);
2020-08-16 14:42:37 +09:00
console.log(
`::warning file=inject_codes.js::Missing source on code '${codeTitle.textContent}' for version ${CODE_VERSIONS[j]}`,
2020-08-16 14:22:48 +09:00
);
}
2020-07-10 10:29:32 +09:00
if (codes[i].querySelectorAll(`source[version='${CODE_VERSIONS[j]}']`).length > 1)
throw new Error(
`Duplicate source on code '${codeTitle.textContent}' for version ${CODE_VERSIONS[j]}`,
);
}
}
};
2020-07-03 08:17:34 +09:00
/**
* Reads the (localized) child node
* @param {*} node The parent node
* @param {*} identifier The childs tag name
* @param {*} lang The target language
* @param {*} fallbackLang The fallback language
*/
const readTextNode = (node, identifier, lang = null, fallbackLang = null) => {
if (!lang) {
const element = node.querySelector(identifier);
if (!element) throw new Error(`${identifier} not found on ${node.textContent}`);
return element.textContent;
}
const localizedElement = node.querySelector(`${identifier}[lang='${lang}']`);
if (localizedElement) return localizedElement.textContent;
2020-07-01 13:55:31 +09:00
2020-07-10 10:29:32 +09:00
const codeTitle = node.querySelector(`title[lang='en-US']`);
console.warn(`No translation found for code '${codeTitle.textContent}' for locale ${lang}`);
2020-08-16 14:42:37 +09:00
console.log(
`::warning file=inject_codes.js::No translation found for code '${codeTitle.textContent}' for locale ${lang}`,
2020-08-16 14:22:48 +09:00
);
2020-07-10 10:29:32 +09:00
2020-07-03 08:17:34 +09:00
if (!fallbackLang) throw new Error(`No localized ${identifier} found on ${node.textContent}`);
const fallbackElement = node.querySelector(`${identifier}[lang='${fallbackLang}']`);
if (fallbackElement) return fallbackElement.textContent;
throw new Error(`No fallback ${identifier} found on ${node.textContent}`);
};
/**
* Reads the presets from the specified code
* @param {*} node The parent node
* @param {*} gameVersion The target game version
* @returns The list of presets
*/
const readPresetList = (node, gameVersion) => {
if (!node) throw new Error('No node provided');
const presets = node.querySelector('presets');
if (!presets || !presets.textContent) return [];
const targetCode = node.querySelector(`source[version='${gameVersion}']`);
if (!targetCode) return [];
const exclusionAttribute = targetCode.getAttribute('exclude-from-presets');
if (exclusionAttribute === 'true') return [];
return presets.textContent.split(',');
};
2020-07-03 08:17:34 +09:00
/**
* Creates an object of localized child nodes
* @param {*} node The parent node
* @param {*} identifier The childs tag name
*/
const localizeNode = (node, identifier) =>
Object.keys(locales).map((locale) => ({
lang: locales[locale].lang,
content: readTextNode(node, identifier, locales[locale].lang, 'en-US'),
}));
/**
* Trims all lines in a multi-line string
* @param {*} str The multiline string
*/
const trimLines = (str) => str.replace(/^ +/gm, '').replace(/ +$/gm, '');
2020-07-10 08:38:15 +09:00
/**
* Localize a markdown text
* @param {*} node The parent node
* @param {*} identifier The childs tag name
*/
2020-07-03 08:17:34 +09:00
const localizeMarkdown = (node, identifier) => {
const markdowns = localizeNode(node, identifier).map((markdown) => ({
...markdown,
content: trimLines(markdown.content),
}));
return markdowns.map((markdown) => ({
...markdown,
html: md.render(markdown.content).html,
}));
};
2020-07-10 08:38:15 +09:00
/**
* Find a code by its version attribute (if specified)
* @param {*} node The parent node
* @param {*} identifier The childs tag name
* @param {*} gameVersion The game version
*/
const readCode = (node, identifier, gameVersion = null) => {
if (!gameVersion) {
const codeNode = node.querySelector(identifier);
return codeNode ? codeNode.textContent.replace(/[\s\n\r\t]+/gm, '') : null;
}
const codeNode = node.querySelector(`${identifier}[version='${gameVersion}']`);
return codeNode ? codeNode.textContent.replace(/[\s\n\r\t]+/gm, '') : null;
};
2020-07-03 08:17:34 +09:00
/**
* Converts the XML source files to a JSON object
* @param {*} xmlString The xml string
2020-07-10 08:38:15 +09:00
* @param {*} gameVersion The game version to filter the code on
2020-07-03 08:17:34 +09:00
*/
2020-07-10 08:38:15 +09:00
const parseXml = (xmlString, gameVersion = null) => {
2020-07-01 15:14:09 +09:00
const codeCollection = new JSDOM(xmlString, {
contentType: 'text/xml',
}).window.document.getElementsByTagName('code');
2020-06-28 06:33:20 +09:00
const codes = [...codeCollection];
2020-07-10 08:38:15 +09:00
return codes
.map((code) => ({
id: code.querySelector('id')?.textContent ?? null,
2020-07-10 08:38:15 +09:00
author: readTextNode(code, 'author'),
title: localizeNode(code, 'title'),
description: localizeMarkdown(code, 'description'),
version: readTextNode(code, 'version'),
date: readTextNode(code, 'date'),
source: readCode(code, 'source', gameVersion),
presets: readPresetList(code, gameVersion),
category: readTextNode(code, 'category'),
dependsOn: code.querySelector('depends-on')?.textContent ?? null,
dependencies:
code.querySelector('dependencies:not([version])')?.textContent.split(',') ??
code.querySelector(`dependencies[version=${gameVersion}]`)?.textContent.split(',') ?? [],
2020-07-10 08:38:15 +09:00
}))
.filter((code) => code.source != null);
2020-06-28 06:33:20 +09:00
};
2020-07-10 10:29:32 +09:00
// Run validation
validateXML(xml);
2020-07-03 08:17:34 +09:00
// Register themes containers such as tip/warning/danger
themePlugins.forEach((p) => {
const container = Array.isArray(p) && p.length === 2 ? p[1] : null;
if (!container) throw new Error();
vuepressContainerPlugin(container).extendMarkdown(md);
});
2020-07-01 15:14:09 +09:00
// Read the current code list
2020-07-10 07:57:16 +09:00
const codeJson = require(JSON_FILE_PATH);
2020-07-01 13:55:31 +09:00
2020-07-01 15:14:09 +09:00
// Populate all code fields in the codeJSON
2020-07-03 06:55:48 +09:00
for (let i = 0; i < CODE_VERSIONS.length; i++) {
2020-07-10 09:39:45 +09:00
codeJson.find((c) => c.identifier === CODE_VERSIONS[i]).codes = parseXml(xml, CODE_VERSIONS[i]);
2020-07-01 13:55:31 +09:00
}
2020-07-01 15:14:09 +09:00
// Save the codeJSON with the updated codes
2020-07-03 06:55:48 +09:00
fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(codeJson));
2020-07-01 14:44:30 +09:00
2021-11-05 10:29:56 +09:00
const parseReferenceList = (xmlString) => {
const codeCollection = new JSDOM(xmlString, {
contentType: 'text/xml',
}).window.document.getElementsByTagName('code');
const codes = [...codeCollection];
return codes.map((code) => ({
author: readTextNode(code, 'author'),
title: localizeNode(code, 'title'),
description: localizeMarkdown(code, 'description'),
version: readTextNode(code, 'version'),
date: readTextNode(code, 'date'),
gameVersions: CODE_VERSIONS.filter(
(v) => code.querySelector(`source[version='${v}']`)?.textContent != null,
),
}));
};
const REFERENCE_CODE_LIST = parseReferenceList(xml);
2020-07-03 08:17:34 +09:00
Object.keys(locales).forEach((locale) => {
const localeKey = locales[locale].lang;
2020-09-25 01:29:56 +09:00
const localeLabels = require(`../site/.vuepress/i18n/${localeKey}.json`);
2020-07-03 08:17:34 +09:00
2021-11-05 10:29:56 +09:00
// Load the target reference file
const referenceFile = path.join(__dirname, `../site/${locale.trim('/')}/code-reference/index.md`);
const referenceContent = fs.readFileSync(referenceFile).toString();
2020-07-03 08:17:34 +09:00
2021-11-05 10:29:56 +09:00
if (!referenceContent.includes(INJECTION_TAG)) {
throw new Error(`No injection tag found in ${locale.trim('/')}/code-reference/index.md`);
}
2020-07-03 08:17:34 +09:00
2021-11-05 10:29:56 +09:00
let fileContent = referenceContent.split(INJECTION_TAG)[0] + INJECTION_TAG;
2020-07-03 08:17:34 +09:00
2021-11-05 10:29:56 +09:00
// Order codes by their localized title
const codes = REFERENCE_CODE_LIST.sort((a, b) =>
a.title.find((t) => t.lang === localeKey).content >
b.title.find((t) => t.lang === localeKey).content
? 1
: -1,
);
// Create a semi-markdown version for all codes
codes.forEach((code) => {
const title = `## ${code.title.find((t) => t.lang === localeKey).content}`;
const author = `*${
code.author.includes(',') ? localeLabels.codeinfo.authors : localeLabels.codeinfo.author
} ${code.author}*`;
const version = `*${localeLabels.codeinfo.version} ${code.version} (${code.date})*`;
const description = code.description.find((d) => d.lang === localeKey).content;
const availableFor = CODE_VERSIONS.filter((v) => code.gameVersions.includes(v))
.map((v) => `<VersionTag version="${v}" supported="true" />`)
.join(' ');
const notAvailableFor = CODE_VERSIONS.filter((v) => !code.gameVersions.includes(v))
.map((v) => `<VersionTag version="${v}" supported="false" />`)
.join(' ');
fileContent += `\n\n${title.trim()}\n\n${availableFor} ${notAvailableFor} \n${version.trim()} \n${author.trim()}\n\n${description.trim()}\n\n`;
});
// Save the reference file
fs.writeFileSync(referenceFile, fileContent);
2020-07-03 08:17:34 +09:00
});