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-10 10:29:32 +09:00
const { title } = require ( 'process' ) ;
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' ) ( {
plugins : [ 'attrs' ] ,
} ) ;
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' ) ) ;
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 } ` ) ;
// 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:22:48 +09:00
console . warn (
` ::warning file=inject_codes.js,line=107,col=10::Missing title translation for code ' ${ codeTitle . textContent } ' and locale ${ localeIdentifiers [ j ] } ` ,
) ;
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:22:48 +09:00
console . warn (
` ::warning file=inject_codes.js,line=122,col=10::Missing description translation for code ' ${ codeTitle . textContent } ' and locale ${ localeIdentifiers [ j ] } ` ,
) ;
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:22:48 +09:00
console . warn (
` ::warning file=inject_codes.js,line=140,col=10::Missing source on code ' ${ codeTitle . textContent } ' for version ${ CODE _VERSIONS [ j ] } ` ,
) ;
}
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:22:48 +09:00
console . warn (
` ::warning file=inject_codes.js,line=172,col=6::No translation found for code ' ${ codeTitle . textContent } ' for locale ${ lang } ` ,
) ;
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 } ` ) ;
} ;
/ * *
* 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 ) => ( {
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 ) ,
} ) )
. 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
2020-07-03 08:17:34 +09:00
Object . keys ( locales ) . forEach ( ( locale ) => {
const localeKey = locales [ locale ] . lang ;
// Populate the code reference
for ( let i = 0 ; i < CODE _VERSIONS . length ; i ++ ) {
// Load the target reference file
const filePath = path . join (
_ _dirname ,
` ../site/ ${ locale . trim ( '/' ) } /code-reference/ ${ CODE _VERSIONS [ i ] . toLowerCase ( ) } .md ` ,
) ;
// Get the current reference
const reference = fs . readFileSync ( filePath ) . toString ( ) ;
if ( ! reference . includes ( INJECTION _TAG ) ) {
throw new Error ( ` No injection tag found in ${ CODE _VERSIONS [ i ] . toLowerCase ( ) } .md ` ) ;
}
// Everything after the injection tag is deleted from the file
let fileContent = reference . split ( INJECTION _TAG ) [ 0 ] + INJECTION _TAG ;
// Order codes by their localized title
const codes = codeJson
. find ( ( c ) => c . identifier === CODE _VERSIONS [ i ] )
. codes . 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 ( ',' ) ? 'Authors:' : 'Author:' } ${ code . author } * ` ;
const version = ` *Version: ${ code . version } ( ${ code . date } )* ` ;
const description = code . description . find ( ( d ) => d . lang === localeKey ) . content ;
fileContent += ` \n \n ${ title . trim ( ) } \n \n ${ version . trim ( ) } \n ${ author . trim ( ) } \n \n ${ description . trim ( ) } \n \n ` ;
} ) ;
// Save the reference file
fs . writeFileSync ( filePath , fileContent ) ;
2020-07-01 14:44:30 +09:00
}
2020-07-03 08:17:34 +09:00
} ) ;