This commit is contained in:
Ulf Gebhardt 2023-04-10 22:07:35 +02:00
parent d1def0cadc
commit e85059b13c
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
9 changed files with 3228 additions and 700 deletions

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
node_modules
**/*.min.js
build

172
.eslintrc.js Normal file
View File

@ -0,0 +1,172 @@
// eslint-disable-next-line import/no-commonjs, import/unambiguous
module.exports = {
root: true,
env: {
node: true,
},
parser: '@typescript-eslint/parser',
plugins: ['prettier', '@typescript-eslint', 'jest', 'import', 'n'],
extends: [
'standard',
'eslint:recommended',
'plugin:prettier/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
],
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
project: ['./tsconfig.json'],
},
node: true,
},
},
rules: {
'no-console': ['error'],
'no-debugger': 'error',
'prettier/prettier': [
'error',
{
htmlWhitespaceSensitivity: 'ignore',
},
],
// jest
'jest/no-disabled-tests': 'error',
'jest/no-focused-tests': 'error',
'jest/no-identical-title': 'error',
'jest/prefer-to-have-length': 'error',
'jest/valid-expect': 'error',
// import
'import/export': 'error',
'import/no-deprecated': 'error',
'import/no-empty-named-blocks': 'error',
'import/no-extraneous-dependencies': 'error',
'import/no-mutable-exports': 'error',
'import/no-unused-modules': 'error',
'import/no-named-as-default': 'error',
'import/no-named-as-default-member': 'error',
'import/no-amd': 'error',
'import/no-commonjs': 'error',
'import/no-import-module-exports': 'error',
'import/no-nodejs-modules': 'off',
'import/unambiguous': 'error',
'import/default': 'error',
'import/named': 'error',
'import/namespace': 'error',
'import/no-absolute-path': 'error',
'import/no-cycle': 'error',
'import/no-dynamic-require': 'error',
'import/no-internal-modules': 'off',
'import/no-relative-packages': 'error',
'import/no-relative-parent-imports': ['error', { ignore: ['@/*'] }],
'import/no-self-import': 'error',
'import/no-unresolved': 'error',
'import/no-useless-path-segments': 'error',
'import/no-webpack-loader-syntax': 'error',
'import/consistent-type-specifier-style': 'error',
'import/exports-last': 'off',
'import/extensions': 'error',
'import/first': 'error',
'import/group-exports': 'off',
'import/newline-after-import': 'error',
'import/no-anonymous-default-export': 'error',
'import/no-default-export': 'error',
'import/no-duplicates': 'error',
'import/no-named-default': 'error',
'import/no-namespace': 'error',
'import/no-unassigned-import': 'error',
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
'newlines-between': 'always',
pathGroups: [
{
pattern: '@?*/**',
group: 'external',
position: 'after',
},
{
pattern: '@/**',
group: 'external',
position: 'after',
},
],
alphabetize: {
order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */,
caseInsensitive: true /* ignore case. Options: [true, false] */,
},
distinctGroup: true,
},
],
'import/prefer-default-export': 'off',
// n
'n/handle-callback-err': 'error',
'n/no-callback-literal': 'error',
'n/no-exports-assign': 'error',
'n/no-extraneous-import': 'error',
'n/no-extraneous-require': 'error',
'n/no-hide-core-modules': 'error',
'n/no-missing-import': 'off', // not compatible with typescript
'n/no-missing-require': 'error',
'n/no-new-require': 'error',
'n/no-path-concat': 'error',
'n/no-process-exit': 'error',
'n/no-unpublished-bin': 'error',
'n/no-unpublished-import': 'off', // TODO need to exclude seeds
'n/no-unpublished-require': 'error',
'n/no-unsupported-features': ['error', { ignores: ['modules'] }],
'n/no-unsupported-features/es-builtins': 'error',
'n/no-unsupported-features/es-syntax': 'error',
'n/no-unsupported-features/node-builtins': 'error',
'n/process-exit-as-throw': 'error',
'n/shebang': 'error',
'n/callback-return': 'error',
'n/exports-style': 'error',
'n/file-extension-in-import': 'off',
'n/global-require': 'error',
'n/no-mixed-requires': 'error',
'n/no-process-env': 'error',
'n/no-restricted-import': 'error',
'n/no-restricted-require': 'error',
'n/no-sync': 'error',
'n/prefer-global/buffer': 'error',
'n/prefer-global/console': 'error',
'n/prefer-global/process': 'error',
'n/prefer-global/text-decoder': 'error',
'n/prefer-global/text-encoder': 'error',
'n/prefer-global/url': 'error',
'n/prefer-global/url-search-params': 'error',
'n/prefer-promises/dns': 'error',
'n/prefer-promises/fs': 'error',
},
overrides: [
// only for ts files
{
files: ['*.ts', '*.tsx'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:@typescript-eslint/strict',
],
rules: {
// allow explicitly defined dangling promises
'@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
'no-void': ['error', { allowAsStatement: true }],
// ignore prefer-regexp-exec rule to allow string.match(regex)
'@typescript-eslint/prefer-regexp-exec': 'off',
// this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486
'import/unambiguous': 'off',
},
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
// this is to properly reference the referenced projects without requirement of compiling it
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
},
},
],
}

9
.prettierrc.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
semi: false,
printWidth: 100,
singleQuote: true,
trailingComma: "all",
tabWidth: 2,
bracketSpacing: true,
endOfLine: "auto",
};

View File

@ -1,4 +1,3 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
// eslint-disable-next-line import/no-commonjs, import/unambiguous // eslint-disable-next-line import/no-commonjs, import/unambiguous
module.exports = { module.exports = {
verbose: true, verbose: true,
@ -10,26 +9,5 @@ module.exports = {
lines: 100, lines: 100,
}, },
}, },
// setupFiles: ['<rootDir>/test/testSetup.ts'],
// setupFilesAfterEnv: ['<rootDir>/test/extensions.ts'],
modulePathIgnorePatterns: ['<rootDir>/build/'], modulePathIgnorePatterns: ['<rootDir>/build/'],
/*moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
'@arg/(.*)': '<rootDir>/src/graphql/arg/$1',
'@enum/(.*)': '<rootDir>/src/graphql/enum/$1',
'@model/(.*)': '<rootDir>/src/graphql/model/$1',
'@union/(.*)': '<rootDir>/src/graphql/union/$1',
'@repository/(.*)': '<rootDir>/src/typeorm/repository/$1',
'@test/(.*)': '<rootDir>/test/$1',
'@entity/(.*)':
// eslint-disable-next-line n/no-process-env
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/entity/$1'
: '<rootDir>/../database/build/entity/$1',
'@dbTools/(.*)':
// eslint-disable-next-line n/no-process-env
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/src/$1'
: '<rootDir>/../database/build/src/$1',
},*/
} }

2880
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,8 @@
"main": "build/index.js", "main": "build/index.js",
"scripts": { "scripts": {
"test": "jest", "test": "jest",
"build": "tsc" "build": "tsc",
"lint": "eslint --max-warnings=0 ."
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -26,8 +27,21 @@
"devDependencies": { "devDependencies": {
"@types/jest": "^29.5.0", "@types/jest": "^29.5.0",
"@types/node": "^18.15.11", "@types/node": "^18.15.11",
"@typescript-eslint/eslint-plugin": "^5.58.0",
"@typescript-eslint/parser": "^5.58.0",
"eslint": "^8.38.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-standard": "^17.0.0",
"eslint-import-resolver-typescript": "^3.5.5",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-prettier": "^4.2.1",
"jest": "^29.5.0", "jest": "^29.5.0",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"typescript": "^5.0.4" "typescript": "^5.0.4"
},
"engines": {
"node": ">=14"
} }
} }

View File

@ -1,439 +1,399 @@
import { open } from 'fs/promises'; import { open } from 'fs/promises'
import { TOC, parse } from './' import path from 'path'
import { TOC, parse } from '.'
describe('parse TOC', () => { describe('parse TOC', () => {
describe('empty file', () => { describe('empty file', () => {
it('returns empty TOC object', async () => { it('returns empty TOC object', async () => {
const file = await open(__dirname + '/../test/empty.toc', 'r'); const file = await open(path.join(__dirname, '../test/empty.toc'), 'r')
const toc = await parse(file) const toc = await parse(file)
expect(toc).toEqual(new TOC()) expect(toc).toEqual(new TOC())
}) })
}) })
describe('basic file', () => { describe('basic file', () => {
it('returns basic TOC object', async () => { it('returns basic TOC object', async () => {
const file = await open(__dirname + '/../test/basic.toc', 'r'); const file = await open(path.join(__dirname, '../test/basic.toc'), 'r')
const toc = await parse(file) const toc = await parse(file)
expect(toc).toEqual({ expect(toc).toEqual({
AddonCompartmentFunc: null, AddonCompartmentFunc: null,
AddonCompartmentFuncOnEnter: null, AddonCompartmentFuncOnEnter: null,
AddonCompartmentFuncOnLeave: null, AddonCompartmentFuncOnLeave: null,
Author: null, Author: null,
DefaultState: true, DefaultState: true,
Dependencies: [], Dependencies: [],
IconAtlas: null, IconAtlas: null,
IconTexture: null, IconTexture: null,
Interface: 50400, Interface: 50400,
LoadManagers: [], LoadManagers: [],
LoadOnDemand: false, LoadOnDemand: false,
LoadWith: [], LoadWith: [],
Notes: "\"Just basic\"", Notes: '"Just basic"',
NotesLocalized: {}, NotesLocalized: {},
OptionalDeps: [], OptionalDeps: [],
SavedVariables: [], SavedVariables: [],
SavedVariablesPerCharacter: [], SavedVariablesPerCharacter: [],
Title: "Basic Toc File", Title: 'Basic Toc File',
TitleLocalized: {}, TitleLocalized: {},
Version: "4.2", Version: '4.2',
files: [ files: ['Dornhoeschen.xml', 'Rapunzel.lua'],
"Dornhoeschen.xml", xTags: {},
"Rapunzel.lua", })
],
xTags: {},
})
}) })
}) })
describe('complete file', () => { describe('complete file', () => {
it('returns complete TOC object', async () => { it('returns complete TOC object', async () => {
const file = await open(__dirname + '/../test/complete.toc', 'r'); const file = await open(path.join(__dirname, '../test/complete.toc'), 'r')
const toc = await parse(file) const toc = await parse(file)
expect(toc).toEqual({ expect(toc).toEqual({
"AddonCompartmentFunc": "Func1", AddonCompartmentFunc: 'Func1',
"AddonCompartmentFuncOnEnter": "Func2", AddonCompartmentFuncOnEnter: 'Func2',
"AddonCompartmentFuncOnLeave": "Func3", AddonCompartmentFuncOnLeave: 'Func3',
"Author": "ulfgebhardt", Author: 'ulfgebhardt',
"DefaultState": false, DefaultState: false,
"Dependencies": [ Dependencies: ['Lib1', 'Lib2', 'Lib3'],
"Lib1", IconAtlas: 'IconB',
"Lib2", IconTexture: 'IconA',
"Lib3", Interface: 123,
], LoadManagers: ['Addon1', 'Addon2', 'Addon3'],
"IconAtlas": "IconB", LoadOnDemand: true,
"IconTexture": "IconA", LoadWith: ['AddonA', 'AddonB', 'AddonC'],
"Interface": 123, Notes: 'Describes the full Toc API',
"LoadManagers": [ NotesLocalized: {
"Addon1", deDE: 'Notes deDE',
"Addon2", enGB: 'Notes enGB',
"Addon3", enUS: 'Notes enUS',
], esES: 'Notes esES',
"LoadOnDemand": true, esMX: 'Notes esMX',
"LoadWith": [ frFR: 'Notes frFR',
"AddonA", itIT: 'Notes itIT',
"AddonB", koKR: 'Notes koKR',
"AddonC", ptBR: 'Notes ptBR',
], ruRU: 'Notes ruRU',
"Notes": "Describes the full Toc API", zhCN: 'Notes zhCN',
"NotesLocalized": { zhTW: 'Notes zhTW',
"deDE": "Notes deDE", },
"enGB": "Notes enGB", OptionalDeps: ['LibA', 'LibB', 'LibC'],
"enUS": "Notes enUS", SavedVariables: ['Var1', 'Var2', 'Var3'],
"esES": "Notes esES", SavedVariablesPerCharacter: ['CVar1', 'CVar2', 'CVar3'],
"esMX": "Notes esMX", Title: 'Complete Toc File',
"frFR": "Notes frFR", TitleLocalized: {
"itIT": "Notes itIT", deDE: 'Title deDE',
"koKR": "Notes koKR", enGB: 'Title enGB',
"ptBR": "Notes ptBR", enUS: 'Title enUS',
"ruRU": "Notes ruRU", esES: 'Title esES',
"zhCN": "Notes zhCN", esMX: 'Title esMX',
"zhTW": "Notes zhTW", frFR: 'Title frFR',
}, itIT: 'Title itIT',
"OptionalDeps": [ koKR: 'Title koKR',
"LibA", ptBR: 'Title ptBR',
"LibB", ruRU: 'Title ruRU',
"LibC", zhCN: 'Title zhCN',
], zhTW: 'Title zhTW',
"SavedVariables": [ },
"Var1", Version: '456',
"Var2", files: ['Dornhoeschen.xml', 'Rapunzel.lua'],
"Var3", xTags: {
], Tag: 'XTag',
"SavedVariablesPerCharacter": [ },
"CVar1", })
"CVar2",
"CVar3",
],
"Title": "Complete Toc File",
"TitleLocalized": {
"deDE": "Title deDE",
"enGB": "Title enGB",
"enUS": "Title enUS",
"esES": "Title esES",
"esMX": "Title esMX",
"frFR": "Title frFR",
"itIT": "Title itIT",
"koKR": "Title koKR",
"ptBR": "Title ptBR",
"ruRU": "Title ruRU",
"zhCN": "Title zhCN",
"zhTW": "Title zhTW",
},
"Version": "456",
"files": [
"Dornhoeschen.xml",
"Rapunzel.lua",
],
"xTags": {
"Tag": "XTag",
}
})
}) })
}) })
describe('Recount file', () => { describe('Recount file', () => {
it('returns Recount TOC object', async () => { it('returns Recount TOC object', async () => {
const file = await open(__dirname + '/../test/Recount.toc', 'r'); const file = await open(path.join(__dirname, '../test/Recount.toc'), 'r')
const toc = await parse(file) const toc = await parse(file)
expect(toc).toEqual({ expect(toc).toEqual({
AddonCompartmentFunc: null, AddonCompartmentFunc: null,
AddonCompartmentFuncOnEnter: null, AddonCompartmentFuncOnEnter: null,
AddonCompartmentFuncOnLeave: null, AddonCompartmentFuncOnLeave: null,
Author: "Cryect, ported to 2.4 by Elsia, maintained by Resike from 5.4", Author: 'Cryect, ported to 2.4 by Elsia, maintained by Resike from 5.4',
DefaultState: true, DefaultState: true,
Dependencies: [], Dependencies: [],
IconAtlas: null, IconAtlas: null,
IconTexture: null, IconTexture: null,
Interface: 50400, Interface: 50400,
LoadManagers: [], LoadManagers: [],
LoadOnDemand: false, LoadOnDemand: false,
LoadWith: [], LoadWith: [],
Notes: "Records Damage and Healing for Graph Based Display.", Notes: 'Records Damage and Healing for Graph Based Display.',
NotesLocalized: { NotesLocalized: {
"ruRU": "Записывает урон и исцеления и отоброжает различные графики.", ruRU: 'Записывает урон и исцеления и отоброжает различные графики.',
"zhCN": "基于 Graph 裤开发的伤害/治疗统计插件.", zhCN: '基于 Graph 裤开发的伤害/治疗统计插件.',
"zhTW": "圖形化顯示的傷害/治療統計插件.", zhTW: '圖形化顯示的傷害/治療統計插件.',
}, },
OptionalDeps: [ OptionalDeps: [
"Ace3", 'Ace3',
"LibDropdown-1.0", 'LibDropdown-1.0',
"LibSharedMedia-3.0", 'LibSharedMedia-3.0',
"LibBossIDs-1.0", 'LibBossIDs-1.0',
"LibGraph-2.0", 'LibGraph-2.0',
], ],
SavedVariables: ["RecountDB"], SavedVariables: ['RecountDB'],
SavedVariablesPerCharacter: ["RecountPerCharDB"], SavedVariablesPerCharacter: ['RecountPerCharDB'],
Title: "Recount", Title: 'Recount',
TitleLocalized: {}, TitleLocalized: {},
Version: "r1269", Version: 'r1269',
files: [ files: [
"embeds.xml", 'embeds.xml',
"locales\\Recount-enUS.lua", 'locales\\Recount-enUS.lua',
"locales\\Recount-deDE.lua", 'locales\\Recount-deDE.lua',
"locales\\Recount-esES.lua", 'locales\\Recount-esES.lua',
"locales\\Recount-esMX.lua", 'locales\\Recount-esMX.lua',
"locales\\Recount-frFR.lua", 'locales\\Recount-frFR.lua',
"locales\\Recount-ptBR.lua", 'locales\\Recount-ptBR.lua',
"locales\\Recount-ruRU.lua", 'locales\\Recount-ruRU.lua',
"locales\\Recount-koKR.lua", 'locales\\Recount-koKR.lua',
"locales\\Recount-zhTW.lua", 'locales\\Recount-zhTW.lua',
"locales\\Recount-zhCN.lua", 'locales\\Recount-zhCN.lua',
"Recount.lua", 'Recount.lua',
"Fonts.lua", 'Fonts.lua',
"colors.lua", 'colors.lua',
"Widgets.lua", 'Widgets.lua',
"WindowOrder.lua", 'WindowOrder.lua',
"Fights.lua", 'Fights.lua',
"Recount_Modes.lua", 'Recount_Modes.lua',
"TrackerModules\\TrackerModule_Dispels.lua", 'TrackerModules\\TrackerModule_Dispels.lua',
"TrackerModules\\TrackerModule_Interrupts.lua", 'TrackerModules\\TrackerModule_Interrupts.lua',
"TrackerModules\\TrackerModule_Resurrection.lua", 'TrackerModules\\TrackerModule_Resurrection.lua',
"TrackerModules\\TrackerModule_CCBreakers.lua", 'TrackerModules\\TrackerModule_CCBreakers.lua',
"TrackerModules\\TrackerModule_PowerGains.lua", 'TrackerModules\\TrackerModule_PowerGains.lua',
"Tracker.lua", 'Tracker.lua',
"roster.lua", 'roster.lua',
"LazySync.lua", 'LazySync.lua',
"deletion.lua", 'deletion.lua',
"zonefilters.lua", 'zonefilters.lua',
"debug.lua", 'debug.lua',
"GUI_Main.lua", 'GUI_Main.lua',
"GUI_Detail.lua", 'GUI_Detail.lua',
"GUI_DeathGraph.lua", 'GUI_DeathGraph.lua',
"GUI_Graph.lua", 'GUI_Graph.lua',
"GUI_Reset.lua", 'GUI_Reset.lua',
"GUI_Report.lua", 'GUI_Report.lua',
"GUI_Config.lua", 'GUI_Config.lua',
"GUI_Realtime.lua" 'GUI_Realtime.lua',
], ],
xTags: { xTags: {
"Curse-Packaged-Version": "r1269", 'Curse-Packaged-Version': 'r1269',
"Curse-Project-ID": "recount", 'Curse-Project-ID': 'recount',
"Curse-Project-Name": "Recount", 'Curse-Project-Name': 'Recount',
"Curse-Repository-ID": "wow/recount/mainline" 'Curse-Repository-ID': 'wow/recount/mainline',
}, },
}) })
}) })
}) })
describe('strict checks', () => { describe('strict checks', () => {
describe('for lines longer then 1024 characters', () => { describe('for lines longer then 1024 characters', () => {
it('fails with strict = true', async () => { it('fails with strict = true', async () => {
const file = await open(__dirname + '/../test/strict.1024.toc', 'r'); const file = await open(path.join(__dirname, '../test/strict.1024.toc'), 'r')
expect(parse(file)).rejects.toThrowError('Line is longer than TOC_LINE_MAX_LENGTH characters, WoW will truncate!') await expect(parse(file)).rejects.toThrowError(
}) 'Line is longer than TOC_LINE_MAX_LENGTH characters, WoW will truncate!',
it('succeeds with strict = false', async () => { )
const file = await open(__dirname + '/../test/strict.1024.toc', 'r'); })
const toc = await parse(file, false) it('succeeds with strict = false', async () => {
expect(toc).toEqual({ const file = await open(path.join(__dirname, '../test/strict.1024.toc'), 'r')
AddonCompartmentFunc: null,
AddonCompartmentFuncOnEnter: null,
AddonCompartmentFuncOnLeave: null,
Author: null,
DefaultState: true,
Dependencies: [],
IconAtlas: null,
IconTexture: null,
Interface: null,
LoadManagers: [],
LoadOnDemand: false,
LoadWith: [],
Notes: null,
NotesLocalized: {},
OptionalDeps: [],
SavedVariables: [],
SavedVariablesPerCharacter: [],
Title: "Strict Check - 1024",
TitleLocalized: {},
Version: null,
files: [
"somefile.lua",
],
xTags: {
"Long": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
},
})
})
})
describe('for double colon', () => {
it('fails with strict = true', async () => {
const file = await open(__dirname + '/../test/strict.doublecolon.toc', 'r');
expect(parse(file)).rejects.toThrowError('Tag could not be parsed: X-Invalid-Tag1,, Weird Tag1')
})
it('succeeds with strict = false', async () => {
const file = await open(__dirname + '/../test/strict.doublecolon.toc', 'r');
const toc = await parse(file, false) const toc = await parse(file, false)
expect(toc).toEqual({ expect(toc).toEqual({
AddonCompartmentFunc: null, AddonCompartmentFunc: null,
AddonCompartmentFuncOnEnter: null, AddonCompartmentFuncOnEnter: null,
AddonCompartmentFuncOnLeave: null, AddonCompartmentFuncOnLeave: null,
Author: null, Author: null,
DefaultState: true, DefaultState: true,
Dependencies: [], Dependencies: [],
IconAtlas: null, IconAtlas: null,
IconTexture: null, IconTexture: null,
Interface: null, Interface: null,
LoadManagers: [], LoadManagers: [],
LoadOnDemand: false, LoadOnDemand: false,
LoadWith: [], LoadWith: [],
Notes: null, Notes: null,
NotesLocalized: {}, NotesLocalized: {},
OptionalDeps: [], OptionalDeps: [],
SavedVariables: [], SavedVariables: [],
SavedVariablesPerCharacter: [], SavedVariablesPerCharacter: [],
Title: "Strict Check - Double Colon", Title: 'Strict Check - 1024',
TitleLocalized: {}, TitleLocalized: {},
Version: null, Version: null,
files: [ files: ['somefile.lua'],
"somefile.lua", xTags: {
], Long: 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd',
xTags: { },
},
}) })
})
}) })
}) describe('for double colon', () => {
describe('for duplicate tag', () => { it('fails with strict = true', async () => {
it('fails with strict = true', async () => { const file = await open(path.join(__dirname, '../test/strict.doublecolon.toc'), 'r')
const file = await open(__dirname + '/../test/strict.duplicate.toc', 'r'); await expect(parse(file)).rejects.toThrowError(
expect(parse(file)).rejects.toThrowError('Duplicate value: X-Duplicate-Tag') 'Tag could not be parsed: X-Invalid-Tag1,, Weird Tag1',
}) )
it('succeeds with strict = false', async () => { })
const file = await open(__dirname + '/../test/strict.duplicate.toc', 'r'); it('succeeds with strict = false', async () => {
const file = await open(path.join(__dirname, '../test/strict.doublecolon.toc'), 'r')
const toc = await parse(file, false) const toc = await parse(file, false)
expect(toc).toEqual({ expect(toc).toEqual({
AddonCompartmentFunc: null, AddonCompartmentFunc: null,
AddonCompartmentFuncOnEnter: null, AddonCompartmentFuncOnEnter: null,
AddonCompartmentFuncOnLeave: null, AddonCompartmentFuncOnLeave: null,
Author: null, Author: null,
DefaultState: true, DefaultState: true,
Dependencies: [], Dependencies: [],
IconAtlas: null, IconAtlas: null,
IconTexture: null, IconTexture: null,
Interface: null, Interface: null,
LoadManagers: [], LoadManagers: [],
LoadOnDemand: false, LoadOnDemand: false,
LoadWith: [], LoadWith: [],
Notes: null, Notes: null,
NotesLocalized: {}, NotesLocalized: {},
OptionalDeps: [], OptionalDeps: [],
SavedVariables: [], SavedVariables: [],
SavedVariablesPerCharacter: [], SavedVariablesPerCharacter: [],
Title: "Strict Check - Duplicate Tag", Title: 'Strict Check - Double Colon',
TitleLocalized: {}, TitleLocalized: {},
Version: null, Version: null,
files: [ files: ['somefile.lua'],
"somefile.lua", xTags: {},
],
xTags: {
"Duplicate-Tag": "Second"
},
}) })
})
}) })
}) describe('for duplicate tag', () => {
describe('for unknown notes locale', () => { it('fails with strict = true', async () => {
it('fails with strict = true', async () => { const file = await open(path.join(__dirname, '../test/strict.duplicate.toc'), 'r')
const file = await open(__dirname + '/../test/strict.locale.notes.toc', 'r'); await expect(parse(file)).rejects.toThrowError('Duplicate value: X-Duplicate-Tag')
expect(parse(file)).rejects.toThrowError('Locale not found: noNO') })
}) it('succeeds with strict = false', async () => {
it('succeeds with strict = false', async () => { const file = await open(path.join(__dirname, '../test/strict.duplicate.toc'), 'r')
const file = await open(__dirname + '/../test/strict.locale.notes.toc', 'r');
const toc = await parse(file, false) const toc = await parse(file, false)
expect(toc).toEqual({ expect(toc).toEqual({
AddonCompartmentFunc: null, AddonCompartmentFunc: null,
AddonCompartmentFuncOnEnter: null, AddonCompartmentFuncOnEnter: null,
AddonCompartmentFuncOnLeave: null, AddonCompartmentFuncOnLeave: null,
Author: null, Author: null,
DefaultState: true, DefaultState: true,
Dependencies: [], Dependencies: [],
IconAtlas: null, IconAtlas: null,
IconTexture: null, IconTexture: null,
Interface: null, Interface: null,
LoadManagers: [], LoadManagers: [],
LoadOnDemand: false, LoadOnDemand: false,
LoadWith: [], LoadWith: [],
Notes: null, Notes: null,
NotesLocalized: {}, NotesLocalized: {},
OptionalDeps: [], OptionalDeps: [],
SavedVariables: [], SavedVariables: [],
SavedVariablesPerCharacter: [], SavedVariablesPerCharacter: [],
Title: "Strict Check - Unknown Notes Locale", Title: 'Strict Check - Duplicate Tag',
TitleLocalized: {}, TitleLocalized: {},
Version: null, Version: null,
files: [ files: ['somefile.lua'],
"somefile.lua", xTags: {
], 'Duplicate-Tag': 'Second',
xTags: { },
},
}) })
})
}) })
}) describe('for unknown notes locale', () => {
describe('for unknown title locale', () => { it('fails with strict = true', async () => {
it('fails with strict = true', async () => { const file = await open(path.join(__dirname, '../test/strict.locale.notes.toc'), 'r')
const file = await open(__dirname + '/../test/strict.locale.title.toc', 'r'); await expect(parse(file)).rejects.toThrowError('Locale not found: noNO')
expect(parse(file)).rejects.toThrowError('Locale not found: noNO') })
}) it('succeeds with strict = false', async () => {
it('succeeds with strict = false', async () => { const file = await open(path.join(__dirname, '../test/strict.locale.notes.toc'), 'r')
const file = await open(__dirname + '/../test/strict.locale.title.toc', 'r');
const toc = await parse(file, false) const toc = await parse(file, false)
expect(toc).toEqual({ expect(toc).toEqual({
AddonCompartmentFunc: null, AddonCompartmentFunc: null,
AddonCompartmentFuncOnEnter: null, AddonCompartmentFuncOnEnter: null,
AddonCompartmentFuncOnLeave: null, AddonCompartmentFuncOnLeave: null,
Author: null, Author: null,
DefaultState: true, DefaultState: true,
Dependencies: [], Dependencies: [],
IconAtlas: null, IconAtlas: null,
IconTexture: null, IconTexture: null,
Interface: null, Interface: null,
LoadManagers: [], LoadManagers: [],
LoadOnDemand: false, LoadOnDemand: false,
LoadWith: [], LoadWith: [],
Notes: null, Notes: null,
NotesLocalized: {}, NotesLocalized: {},
OptionalDeps: [], OptionalDeps: [],
SavedVariables: [], SavedVariables: [],
SavedVariablesPerCharacter: [], SavedVariablesPerCharacter: [],
Title: "Strict Check - Unknown Title Locale", Title: 'Strict Check - Unknown Notes Locale',
TitleLocalized: {}, TitleLocalized: {},
Version: null, Version: null,
files: [ files: ['somefile.lua'],
"somefile.lua", xTags: {},
],
xTags: {
},
}) })
})
}) })
}) describe('for unknown title locale', () => {
describe('for unknown tag', () => { it('fails with strict = true', async () => {
it('fails with strict = true', async () => { const file = await open(path.join(__dirname, '../test/strict.locale.title.toc'), 'r')
const file = await open(__dirname + '/../test/strict.unknowntag.toc', 'r'); await expect(parse(file)).rejects.toThrowError('Locale not found: noNO')
expect(parse(file)).rejects.toThrowError('Unknown Tag name: SomeTag') })
}) it('succeeds with strict = false', async () => {
it('succeeds with strict = false', async () => { const file = await open(path.join(__dirname, '../test/strict.locale.title.toc'), 'r')
const file = await open(__dirname + '/../test/strict.unknowntag.toc', 'r');
const toc = await parse(file, false) const toc = await parse(file, false)
expect(toc).toEqual({ expect(toc).toEqual({
AddonCompartmentFunc: null, AddonCompartmentFunc: null,
AddonCompartmentFuncOnEnter: null, AddonCompartmentFuncOnEnter: null,
AddonCompartmentFuncOnLeave: null, AddonCompartmentFuncOnLeave: null,
Author: null, Author: null,
DefaultState: true, DefaultState: true,
Dependencies: [], Dependencies: [],
IconAtlas: null, IconAtlas: null,
IconTexture: null, IconTexture: null,
Interface: null, Interface: null,
LoadManagers: [], LoadManagers: [],
LoadOnDemand: false, LoadOnDemand: false,
LoadWith: [], LoadWith: [],
Notes: null, Notes: null,
NotesLocalized: {}, NotesLocalized: {},
OptionalDeps: [], OptionalDeps: [],
SavedVariables: [], SavedVariables: [],
SavedVariablesPerCharacter: [], SavedVariablesPerCharacter: [],
Title: "Strict Check - Unknown Tag", Title: 'Strict Check - Unknown Title Locale',
TitleLocalized: {}, TitleLocalized: {},
Version: null, Version: null,
files: [ files: ['somefile.lua'],
"somefile.lua", xTags: {},
],
xTags: {
},
}) })
})
}) })
}) describe('for unknown tag', () => {
}) it('fails with strict = true', async () => {
const file = await open(path.join(__dirname, '../test/strict.unknowntag.toc'), 'r')
await expect(parse(file)).rejects.toThrowError('Unknown Tag name: SomeTag')
})
it('succeeds with strict = false', async () => {
const file = await open(path.join(__dirname, '../test/strict.unknowntag.toc'), 'r')
const toc = await parse(file, false)
expect(toc).toEqual({
AddonCompartmentFunc: null,
AddonCompartmentFuncOnEnter: null,
AddonCompartmentFuncOnLeave: null,
Author: null,
DefaultState: true,
Dependencies: [],
IconAtlas: null,
IconTexture: null,
Interface: null,
LoadManagers: [],
LoadOnDemand: false,
LoadWith: [],
Notes: null,
NotesLocalized: {},
OptionalDeps: [],
SavedVariables: [],
SavedVariablesPerCharacter: [],
Title: 'Strict Check - Unknown Tag',
TitleLocalized: {},
Version: null,
files: ['somefile.lua'],
xTags: {},
})
})
})
})
}) })

View File

@ -1,4 +1,4 @@
import { FileHandle } from 'fs/promises'; import { FileHandle } from 'fs/promises'
export const TOC_PREFIX_TAG = '## ' export const TOC_PREFIX_TAG = '## '
export const TOC_PREFIX_COMMENT = '#' export const TOC_PREFIX_COMMENT = '#'
@ -51,20 +51,20 @@ export enum WOW_LOCALES {
export class TOC { export class TOC {
Interface: number | null = null Interface: number | null = null
Title: string | null = null Title: string | null = null
TitleLocalized: Partial<Record<WOW_LOCALES,string>> = {} TitleLocalized: Partial<Record<WOW_LOCALES, string>> = {}
Notes: string | null = null Notes: string | null = null
NotesLocalized: Partial<Record<WOW_LOCALES,string>> = {} NotesLocalized: Partial<Record<WOW_LOCALES, string>> = {}
IconTexture: string | null = null IconTexture: string | null = null
IconAtlas: string | null = null IconAtlas: string | null = null
AddonCompartmentFunc: string | null = null AddonCompartmentFunc: string | null = null
AddonCompartmentFuncOnEnter: string | null = null AddonCompartmentFuncOnEnter: string | null = null
AddonCompartmentFuncOnLeave: string | null = null AddonCompartmentFuncOnLeave: string | null = null
LoadOnDemand: boolean = false LoadOnDemand = false
Dependencies: string[] = [] Dependencies: string[] = []
OptionalDeps: string[] = [] OptionalDeps: string[] = []
LoadWith: string[] = [] LoadWith: string[] = []
LoadManagers: string[] = [] LoadManagers: string[] = []
DefaultState: boolean = true DefaultState = true
SavedVariables: string[] = [] SavedVariables: string[] = []
SavedVariablesPerCharacter: string[] = [] SavedVariablesPerCharacter: string[] = []
Author: string | null = null Author: string | null = null
@ -73,19 +73,19 @@ export class TOC {
files: string[] = [] files: string[] = []
} }
export const parse = async function(file: FileHandle, strict = true) { export const parse = async function (file: FileHandle, strict = true) {
const result = new TOC(); const result = new TOC()
const allTags: Record<string, string> = {} const allTags: Record<string, string> = {}
for await (const line of file.readLines()) { for await (const line of file.readLines()) {
if (strict && line.length > TOC_LINE_MAX_LENGTH) { if (strict && line.length > TOC_LINE_MAX_LENGTH) {
throw new Error('Line is longer than TOC_LINE_MAX_LENGTH characters, WoW will truncate!'); throw new Error('Line is longer than TOC_LINE_MAX_LENGTH characters, WoW will truncate!')
} }
if (line.slice(0, TOC_PREFIX_TAG.length) === TOC_PREFIX_TAG) { if (line.startsWith(TOC_PREFIX_TAG)) {
const tagData = line.slice(TOC_PREFIX_TAG.length).split(TOC_TAG_DELIMITER) const tagData = line.slice(TOC_PREFIX_TAG.length).split(TOC_TAG_DELIMITER)
if(tagData.length !== 2){ if (tagData.length !== 2) {
if(strict){ if (strict) {
throw new Error(`Tag could not be parsed: ${tagData}`) throw new Error(`Tag could not be parsed: ${tagData.join(',')}`)
} else { } else {
continue continue
} }
@ -93,21 +93,21 @@ export const parse = async function(file: FileHandle, strict = true) {
const tagName = tagData[0].trim() const tagName = tagData[0].trim()
const tagValue = tagData[1].trim() const tagValue = tagData[1].trim()
if(strict && allTags[tagName]){ if (strict && allTags[tagName]) {
throw new Error(`Duplicate value: ${tagName}`) throw new Error(`Duplicate value: ${tagName}`)
} }
allTags[tagName] = tagValue; allTags[tagName] = tagValue
switch(tagName.toLowerCase()){ switch (tagName.toLowerCase()) {
case TOC_TAG_NAME_INTERFACE.toLowerCase(): case TOC_TAG_NAME_INTERFACE.toLowerCase():
result.Interface = Number(tagValue) result.Interface = Number(tagValue)
break; break
case TOC_TAG_NAME_TITLE.toLowerCase(): case TOC_TAG_NAME_TITLE.toLowerCase():
result.Title = tagValue result.Title = tagValue
break; break
case TOC_TAG_NAME_NOTES.toLocaleLowerCase(): case TOC_TAG_NAME_NOTES.toLocaleLowerCase():
result.Notes = tagValue result.Notes = tagValue
break break
case TOC_TAG_NAME_ICONTEXTURE.toLowerCase(): case TOC_TAG_NAME_ICONTEXTURE.toLowerCase():
result.IconTexture = tagValue result.IconTexture = tagValue
@ -143,7 +143,9 @@ export const parse = async function(file: FileHandle, strict = true) {
result.SavedVariables = tagValue.split(TOC_TAG_LIST_DELIMITER).map((s) => s.trim()) result.SavedVariables = tagValue.split(TOC_TAG_LIST_DELIMITER).map((s) => s.trim())
break break
case TOC_TAG_NAME_SAVEDVARIABESPERCHARACTER.toLowerCase(): case TOC_TAG_NAME_SAVEDVARIABESPERCHARACTER.toLowerCase():
result.SavedVariablesPerCharacter = tagValue.split(TOC_TAG_LIST_DELIMITER).map((s) => s.trim()) result.SavedVariablesPerCharacter = tagValue
.split(TOC_TAG_LIST_DELIMITER)
.map((s) => s.trim())
break break
case TOC_TAG_NAME_AUTHOR.toLowerCase(): case TOC_TAG_NAME_AUTHOR.toLowerCase():
result.Author = tagValue result.Author = tagValue
@ -152,31 +154,43 @@ export const parse = async function(file: FileHandle, strict = true) {
result.Version = tagValue result.Version = tagValue
break break
default: default:
if(tagName.toLowerCase().startsWith(TOC_TAG_PREFIX_TITLELOCALIZED) && tagName.length === TOC_TAG_PREFIX_TITLELOCALIZED.length+4){ if (
const locale = tagName.slice(TOC_TAG_PREFIX_TITLELOCALIZED.length,TOC_TAG_PREFIX_TITLELOCALIZED.length+4) tagName.toLowerCase().startsWith(TOC_TAG_PREFIX_TITLELOCALIZED) &&
if(Object.keys(WOW_LOCALES).includes(locale)){ tagName.length === TOC_TAG_PREFIX_TITLELOCALIZED.length + 4
) {
const locale = tagName.slice(
TOC_TAG_PREFIX_TITLELOCALIZED.length,
TOC_TAG_PREFIX_TITLELOCALIZED.length + 4,
)
if (Object.keys(WOW_LOCALES).includes(locale)) {
result.TitleLocalized[locale as WOW_LOCALES] = tagValue result.TitleLocalized[locale as WOW_LOCALES] = tagValue
} else if(strict){ } else if (strict) {
throw new Error(`Locale not found: ${locale}`) throw new Error(`Locale not found: ${locale}`)
} }
} else if(tagName.toLowerCase().startsWith(TOC_TAG_PREFIX_NOTESLOCALIZED) && tagName.length === TOC_TAG_PREFIX_NOTESLOCALIZED.length+4){ } else if (
const locale = tagName.slice(TOC_TAG_PREFIX_NOTESLOCALIZED.length,TOC_TAG_PREFIX_NOTESLOCALIZED.length+4) tagName.toLowerCase().startsWith(TOC_TAG_PREFIX_NOTESLOCALIZED) &&
if(Object.keys(WOW_LOCALES).includes(locale)){ tagName.length === TOC_TAG_PREFIX_NOTESLOCALIZED.length + 4
) {
const locale = tagName.slice(
TOC_TAG_PREFIX_NOTESLOCALIZED.length,
TOC_TAG_PREFIX_NOTESLOCALIZED.length + 4,
)
if (Object.keys(WOW_LOCALES).includes(locale)) {
result.NotesLocalized[locale as WOW_LOCALES] = tagValue result.NotesLocalized[locale as WOW_LOCALES] = tagValue
} else if(strict){ } else if (strict) {
throw new Error(`Locale not found: ${locale}`) throw new Error(`Locale not found: ${locale}`)
} }
} else if(tagName.toLowerCase().startsWith(TOC_TAG_PREFIX_DEP)){ } else if (tagName.toLowerCase().startsWith(TOC_TAG_PREFIX_DEP)) {
result.Dependencies = tagValue.split(TOC_TAG_LIST_DELIMITER).map((s) => s.trim()) result.Dependencies = tagValue.split(TOC_TAG_LIST_DELIMITER).map((s) => s.trim())
} else if(tagName.toLowerCase().startsWith(TOC_TAG_PREFIX_X)){ } else if (tagName.toLowerCase().startsWith(TOC_TAG_PREFIX_X)) {
result.xTags[tagName.slice(TOC_TAG_PREFIX_X.length)] = tagValue result.xTags[tagName.slice(TOC_TAG_PREFIX_X.length)] = tagValue
} else if(strict){ } else if (strict) {
throw new Error(`Unknown Tag name: ${tagName}`) throw new Error(`Unknown Tag name: ${tagName}`)
} }
} }
} else if (line.slice(0, 1) !== TOC_PREFIX_COMMENT && line.trim()) { } else if (!line.startsWith(TOC_PREFIX_COMMENT) && line.trim()) {
result.files.push(line.trimEnd()); result.files.push(line.trimEnd())
} }
} }
return result; return result
}; }

View File

@ -28,7 +28,7 @@
"module": "commonjs", /* Specify what module code is generated. */ "module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */ // "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
@ -105,5 +105,5 @@
/* Completeness */ /* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */
} },
} }