├── test ├── locales │ └── en │ │ ├── test.yaml │ │ ├── test.json │ │ ├── test.json5 │ │ ├── test.jsonc │ │ ├── test.js │ │ ├── test-with-comments.json5 │ │ └── test-with-comments.jsonc ├── typescript │ └── basic.test-d.ts ├── bun │ ├── backend.yaml.test.js │ ├── backend.json.test.js │ ├── backend.caching.test.js │ ├── backend.js.test.js │ ├── backend.json5.test.js │ └── backend.jsonc.test.js ├── backend.yaml.js ├── backend.json.js ├── backend.js.js ├── deno │ ├── backend.json.js │ ├── backend.yaml.js │ ├── backend.js.js │ ├── backend.caching.js │ ├── backend.json5.js │ └── backend.jsonc.js ├── backend.caching.js ├── backend.json5.js └── backend.jsonc.js ├── index.js ├── .gitignore ├── example ├── caching │ ├── locales │ │ ├── de │ │ │ └── translation.json │ │ └── en │ │ │ └── translation.json │ ├── package.json │ └── app.js ├── deno │ ├── locales │ │ ├── de │ │ │ └── translation.json │ │ └── en │ │ │ └── translation.json │ └── index.js ├── fallback │ ├── locales │ │ ├── de │ │ │ └── translation.json │ │ └── en │ │ │ └── translation.json │ ├── locales_fallback │ │ ├── de │ │ │ └── translation.json │ │ └── en │ │ │ └── translation.json │ ├── package.json │ └── app.js ├── node │ ├── locales │ │ ├── de │ │ │ └── translation.json │ │ └── en │ │ │ └── translation.json │ ├── package.json │ └── index.js ├── updatable-cache │ ├── locales │ │ ├── de │ │ │ └── translation.json │ │ └── en │ │ │ └── translation.json │ ├── package.json │ └── app.js └── fastify │ ├── README.md │ ├── lambda.js │ ├── views │ └── index.pug │ ├── lib │ ├── mail.js │ └── i18n.js │ ├── locales │ ├── en │ │ └── translation.json │ ├── it │ │ └── translation.json │ └── de │ │ └── translation.json │ ├── mails │ ├── invitation.pug │ └── layout.pug │ ├── package.json │ ├── app.js │ └── app-sam.json ├── .eslintignore ├── tslint.json ├── index.d.mts ├── lib ├── extname.js ├── utils.js ├── readFile.js ├── writeFile.js ├── index.js └── formats │ ├── json5.js │ └── jsonc.js ├── .travis.yml ├── .npmignore ├── .editorconfig ├── CHANGELOG.md ├── .eslintrc ├── tsconfig.json ├── .babelrc ├── .github ├── workflows │ ├── node.yml │ └── deno.yml └── stale.yml ├── licence ├── index.d.ts ├── package.json └── README.md /test/locales/en/test.yaml: -------------------------------------------------------------------------------- 1 | key: passing 2 | -------------------------------------------------------------------------------- /test/locales/en/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "passing" 3 | } -------------------------------------------------------------------------------- /test/locales/en/test.json5: -------------------------------------------------------------------------------- 1 | { 2 | key: 'passing', 3 | } -------------------------------------------------------------------------------- /test/locales/en/test.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "key": "passing" 3 | } -------------------------------------------------------------------------------- /test/locales/en/test.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "key": "passing" 3 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import backend from './lib/index.js' 2 | export default backend 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | node_modules 3 | package-lock.json 4 | cjs 5 | esm 6 | deno.lock -------------------------------------------------------------------------------- /example/caching/locales/de/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hallo welt" 3 | } -------------------------------------------------------------------------------- /example/caching/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hello world" 3 | } -------------------------------------------------------------------------------- /example/deno/locales/de/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hallo welt" 3 | } 4 | -------------------------------------------------------------------------------- /example/deno/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hello world" 3 | } 4 | -------------------------------------------------------------------------------- /example/fallback/locales/de/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hallo welt" 3 | } -------------------------------------------------------------------------------- /example/fallback/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hello world" 3 | } -------------------------------------------------------------------------------- /example/node/locales/de/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hallo welt" 3 | } 4 | -------------------------------------------------------------------------------- /example/node/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hello world" 3 | } 4 | -------------------------------------------------------------------------------- /example/updatable-cache/locales/de/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hallo welt" 3 | } -------------------------------------------------------------------------------- /example/updatable-cache/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hello world" 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build 2 | /node_modules 3 | /cjs 4 | /esm 5 | /dist 6 | /example 7 | /lib/formats 8 | /test -------------------------------------------------------------------------------- /example/fallback/locales_fallback/de/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hallo welt vom lokalen fallback" 3 | } 4 | -------------------------------------------------------------------------------- /example/fallback/locales_fallback/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": "hello world from local fallback" 3 | } 4 | -------------------------------------------------------------------------------- /example/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs", 3 | "dependencies": { 4 | "i18next": "24.0.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/fastify/README.md: -------------------------------------------------------------------------------- 1 | # run the sample 2 | 3 | ``` 4 | $ npm i 5 | $ npm start 6 | ``` 7 | 8 | open: http://localhost:8080 -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": "dtslint/dtslint.json", 4 | "rules": { 5 | "semicolon": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /index.d.mts: -------------------------------------------------------------------------------- 1 | import * as index from './index.js'; 2 | 3 | export default index.default; 4 | 5 | export type FsBackendOptions = index.FsBackendOptions; 6 | -------------------------------------------------------------------------------- /lib/extname.js: -------------------------------------------------------------------------------- 1 | export default (filename) => { 2 | if (filename.indexOf('.') < 0) return undefined 3 | return `.${filename.split('.').pop()}` 4 | } 5 | -------------------------------------------------------------------------------- /example/fastify/lambda.js: -------------------------------------------------------------------------------- 1 | import awsLambdaFastify from '@fastify/aws-lambda' 2 | import app from './app.js' 3 | export const handler = awsLambdaFastify(app) 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '12' 5 | - '14' 6 | branches: 7 | only: 8 | - master 9 | notifications: 10 | email: 11 | - adriano@raiano.ch -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | test 3 | dist 4 | .gitignore 5 | .editorconfig 6 | .eslintignore 7 | .eslintrc 8 | .travis.yml 9 | .github 10 | package-lock.json 11 | README.md 12 | tsconfig.json 13 | example 14 | .babelrc 15 | deno.lock -------------------------------------------------------------------------------- /test/typescript/basic.test-d.ts: -------------------------------------------------------------------------------- 1 | import i18next from 'i18next'; 2 | import Backend from 'i18next-fs-backend'; 3 | 4 | i18next.use(Backend).init({ 5 | backend: { 6 | loadPath: '/locales/{{lng}}/{{ns}}.json' 7 | }, 8 | }); 9 | -------------------------------------------------------------------------------- /test/locales/en/test-with-comments.json5: -------------------------------------------------------------------------------- 1 | { 2 | "key": "passing", 3 | // line comment 4 | "commented": "value", /* inline block */ 5 | /* block comment 6 | multiple lines */ 7 | "block": "value" 8 | } -------------------------------------------------------------------------------- /test/locales/en/test-with-comments.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "key": "passing", 3 | // line comment 4 | "commented": "value", /* inline block */ 5 | /* block comment 6 | multiple lines */ 7 | "block": "value" 8 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | [*.{js,jsx,json}] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | charset = utf-8 8 | indent_style = space 9 | indent_size = 2 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /example/caching/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs", 3 | "dependencies": { 4 | "i18next": "23.16.6", 5 | "i18next-chained-backend": "4.6.2", 6 | "i18next-fs-backend": "2.4.0", 7 | "i18next-http-backend": "2.6.2" 8 | }, 9 | "devDependencies": { 10 | "express": "4.21.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/fallback/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs", 3 | "dependencies": { 4 | "i18next": "20.3.0", 5 | "i18next-chained-backend": "3.0.2", 6 | "i18next-fs-backend": "1.1.1", 7 | "i18next-http-backend": "1.2.4" 8 | }, 9 | "devDependencies": { 10 | "express": "4.20.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/updatable-cache/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs", 3 | "dependencies": { 4 | "i18next": "23.16.6", 5 | "i18next-chained-backend": "4.6.2", 6 | "i18next-fs-backend": "2.4.0", 7 | "i18next-http-backend": "2.6.2" 8 | }, 9 | "devDependencies": { 10 | "express": "4.21.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 2.6.1 2 | 3 | - Bump js-yaml from 4.1.0 to 4.1.1 (#64) 4 | 5 | ### 2.6.0 6 | 7 | - support `initImmediate` -> `initAsync` renaming of i18next v24 8 | 9 | ### 2.5.0 10 | 11 | - fix for Deno 2 and removal of unnecessary .cjs file 12 | - for esm build environments not supporting top-level await, you should import the `i18next-fs-backend/cjs` export or stay at v2.4.0 13 | -------------------------------------------------------------------------------- /example/fastify/views/index.pug: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title i18next - fastify with pug 4 | body 5 | h1=t('home.title') 6 | div 7 | a(href="/?lng=en") english 8 | |   |   9 | a(href="/?lng=it") italiano 10 | |   |   11 | a(href="/?lng=de") deutsch 12 | hr 13 | div 14 | a(href="/email") email test 15 | |   |   16 | a(href="/raw") raw test 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "standard", 4 | "plugin:require-path-exists/recommended" 5 | ], 6 | "plugins": [ 7 | "require-path-exists" 8 | ], 9 | "globals": { 10 | "describe": false, 11 | "it": false, 12 | "before": false, 13 | "after": false, 14 | "beforeEach": false, 15 | "afterEach": false, 16 | "Deno": false, 17 | "Bun": false 18 | }, 19 | "rules": { 20 | "array-bracket-spacing": 0, 21 | "standard/no-callback-literal": 0 22 | } 23 | } -------------------------------------------------------------------------------- /example/fastify/lib/mail.js: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'path' 2 | import { fileURLToPath } from 'url' 3 | import pug from 'pug' 4 | import mjml2html from 'mjml' 5 | 6 | const __dirname = dirname(fileURLToPath(import.meta.url)) 7 | 8 | export default (template, data) => { 9 | const mjml = pug.renderFile(join(__dirname, '../mails', `${template}.pug`), data) 10 | const { html, errors } = mjml2html(mjml) 11 | if (errors && errors.length > 0) throw new Error(errors[0].message) 12 | return html 13 | } 14 | -------------------------------------------------------------------------------- /example/fastify/locales/en/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": { 3 | "title": "Hello World!" 4 | }, 5 | "server": { 6 | "started": "Server is listening on port {{port}}." 7 | }, 8 | "invitation": { 9 | "accept": "accept invitation", 10 | "endText": "Having questions? Just contact us at {{email}}.", 11 | "greeting": "Hi {{firstname}},", 12 | "salutation": "Best regards, Your AWESOME-PRODUCT team", 13 | "subject": "Invitation to AWESOME-PRODUCT", 14 | "text": "You were invited to AWESOME-PRODUCT" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/fastify/locales/it/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": { 3 | "title": "Ciao Mondo!" 4 | }, 5 | "server": { 6 | "started": "Il server sta aspettando sul port {{port}}." 7 | }, 8 | "invitation": { 9 | "accept": "accetta l'invito", 10 | "endText": "Hai domande? Contattaci con {{email}}.", 11 | "greeting": "Ciao {{firstname}},", 12 | "salutation": "Cordiali saluti, Il team di AWESOME-PRODUCT", 13 | "subject": "Invito per AWESOME-PRODUCT", 14 | "text": "Sei stato invitate per AWESOME-PRODUCT" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "lib": ["es6", "dom"], 6 | "jsx": "react", 7 | "moduleResolution": "node", 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "noEmit": true, 11 | "baseUrl": ".", 12 | "paths": { "i18next-fs-backend": ["./index.d.mts"] }, 13 | 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | "include": ["./index.d.mts", "./test/**/*"], 18 | "exclude": [] 19 | } 20 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "esm": { 4 | "presets": [ 5 | ["@babel/preset-env", { 6 | "modules": false, 7 | "targets": { 8 | "esmodules": true, 9 | "ie": 11 10 | } 11 | }] 12 | ] 13 | }, 14 | "cjs": { 15 | "plugins": ["add-module-exports"], 16 | "presets": [ 17 | ["@babel/preset-env", { 18 | "targets": { 19 | "ie": 11 20 | } 21 | }] 22 | ] 23 | } 24 | }, 25 | "comments": false 26 | } 27 | -------------------------------------------------------------------------------- /example/fastify/locales/de/translation.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": { 3 | "title": "Hallo Welt!" 4 | }, 5 | "server": { 6 | "started": "Der server lauscht auf dem Port {{port}}." 7 | }, 8 | "invitation": { 9 | "accept": "akzeptiere die Einladung", 10 | "endText": "Hast du Fragen? Dann kontaktiere uns via {{email}}.", 11 | "greeting": "Hallo {{firstname}},", 12 | "salutation": "Freundliche Grüsse, Dein AWESOME-PRODUCT Team", 13 | "subject": "Einladung zu AWESOME-PRODUCT", 14 | "text": "Du bist zum AWESOME-PRODUCT eingeladen worden" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: node 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | test: 10 | name: Test on node ${{ matrix.node }} and ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | node: [ '20.x', '18.x', '16.x' ] 15 | # os: [ubuntu-latest, windows-latest, macOS-latest] 16 | os: [ubuntu-latest] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node }} 23 | - run: npm install 24 | - run: npm test -------------------------------------------------------------------------------- /example/fastify/mails/invitation.pug: -------------------------------------------------------------------------------- 1 | extends ./layout.pug 2 | 3 | block content 4 | mj-text(align='left', color='#ffffff', font-size='16px', font-family='open Sans Helvetica, Arial, sans-serif', padding-left='25px', padding-right='25px') 5 | =t('invitation.text') 6 | mj-button(href=invitationLink, align='left', font-size='16px', background-color='#024b3f', border-radius='5px', color='#ffffff', font-family='open Sans Helvetica, Arial, sans-serif')=t('invitation.accept') 7 | mj-text(align='left', color='#ffffff', font-size='16px', font-family='open Sans Helvetica, Arial, sans-serif', padding-left='25px', padding-right='25px') 8 | =t('invitation.endText', { email: 'support@awesome-product.com' }) 9 | -------------------------------------------------------------------------------- /.github/workflows/deno.yml: -------------------------------------------------------------------------------- 1 | name: deno 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | test: 10 | name: Test on deno ${{ matrix.deno }} and ${{ matrix.os }} 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | deno: [ '2.x', '1.x' ] 15 | # os: [ubuntu-latest, windows-latest, macOS-latest] 16 | os: [ubuntu-latest] 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Setup Deno 20 | uses: denolib/setup-deno@master 21 | with: 22 | deno-version: ${{ matrix.deno }} 23 | - run: deno --version 24 | - run: deno test test/deno/*.js --allow-read --allow-write --no-check 25 | -------------------------------------------------------------------------------- /example/node/index.js: -------------------------------------------------------------------------------- 1 | const i18next = require('i18next') 2 | const Backend = require('../../cjs') 3 | 4 | i18next.use(Backend).init({ 5 | initAsync: false, 6 | lng: 'en', 7 | fallbackLng: 'en', 8 | preload: ['en', 'de'], 9 | ns: ['translation'], 10 | defaultNS: 'translation', 11 | backend: { 12 | loadPath: 'locales/{{lng}}/{{ns}}.json' 13 | } 14 | }, (err, t) => { 15 | if (err) return console.error(err) 16 | console.log('i18next is ready...') 17 | console.log(t('welcome')) 18 | console.log(t('welcome', { lng: 'de' })) 19 | }) 20 | // this will only work if initAsync is set to false, because it's async 21 | console.log(i18next.t('welcome')) 22 | console.log(i18next.t('welcome', { lng: 'de' })) 23 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Number of days of inactivity before an issue becomes stale 2 | daysUntilStale: 7 3 | # Number of days of inactivity before a stale issue is closed 4 | daysUntilClose: 7 5 | # Issues with these labels will never be considered stale 6 | exemptLabels: 7 | - "discussion" 8 | - "feature request" 9 | - "bug" 10 | - "breaking change" 11 | - "doc" 12 | - "issue" 13 | - "help wanted" 14 | - "good first issue" 15 | # Label to use when marking an issue as stale 16 | staleLabel: stale 17 | # Comment to post when marking an issue as stale. Set to `false` to disable 18 | markComment: > 19 | This issue has been automatically marked as stale because it has not had 20 | recent activity. It will be closed if no further activity occurs. Thank you 21 | for your contributions. 22 | # Comment to post when closing a stale issue. Set to `false` to disable 23 | closeComment: false 24 | -------------------------------------------------------------------------------- /example/deno/index.js: -------------------------------------------------------------------------------- 1 | import i18next from 'https://deno.land/x/i18next/index.js' 2 | import Backend from 'https://deno.land/x/i18next_fs_backend/index.js' 3 | // import Backend from 'https://cdn.jsdelivr.net/gh/i18next/i18next-fs-backend/index.js' 4 | // import Backend from 'https://raw.githubusercontent.com/i18next/i18next-fs-backend/master/index.js' 5 | // import Backend from '../../lib/index.js' 6 | 7 | i18next.use(Backend).init({ 8 | // initAsync: false, 9 | lng: 'en', 10 | fallbackLng: 'en', 11 | preload: ['en', 'de'], 12 | ns: ['translation'], 13 | defaultNS: 'translation', 14 | backend: { 15 | loadPath: 'locales/{{lng}}/{{ns}}.json' 16 | } 17 | }, (err, t) => { 18 | if (err) return console.error(err) 19 | console.log('i18next is ready...') 20 | console.log(t('welcome')) 21 | console.log(t('welcome', { lng: 'de' })) 22 | }) 23 | // this will only work if initAsync is set to false, because it's async 24 | console.log(i18next.t('welcome')) 25 | console.log(i18next.t('welcome', { lng: 'de' })) 26 | -------------------------------------------------------------------------------- /example/fastify/lib/i18n.js: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'path' 2 | import { readdirSync, lstatSync } from 'fs' 3 | import { fileURLToPath } from 'url' 4 | import i18next from 'i18next' 5 | import Backend from 'i18next-fs-backend' 6 | import i18nextMiddleware from 'i18next-http-middleware' 7 | 8 | const __dirname = dirname(fileURLToPath(import.meta.url)) 9 | const localesFolder = join(__dirname, '../locales') 10 | 11 | i18next 12 | .use(i18nextMiddleware.LanguageDetector) 13 | .use(Backend) 14 | .init({ 15 | // debug: true, 16 | initAsync: false, // setting initImediate to false, will load the resources synchronously 17 | fallbackLng: 'en', 18 | preload: readdirSync(localesFolder).filter((fileName) => { 19 | const joinedPath = join(localesFolder, fileName) 20 | return lstatSync(joinedPath).isDirectory() 21 | }), 22 | backend: { 23 | loadPath: join(localesFolder, '{{lng}}/{{ns}}.json') 24 | } 25 | }) 26 | 27 | export function register (app) { 28 | app.register(i18nextMiddleware.plugin, { i18next }) 29 | } 30 | 31 | export { i18next } 32 | -------------------------------------------------------------------------------- /licence: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 i18next 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /example/fastify/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fastify-example-app", 3 | "type": "module", 4 | "scripts": { 5 | "start": "node app.js", 6 | "build": "rm -rf build && packageName=$(npm pack) && tar -xvzf $packageName && mv package build && rm $packageName && cd build && npm i --production && find ./node_modules/* -mtime +10950 -exec touch {} \\; && rm -f package-lock.json && cd ..", 7 | "cf_package": "npm run build && aws cloudformation package --template-file app-sam.json --s3-bucket my-cloudformation-code-artifacts --output-template-file app-output_sam.yaml", 8 | "cf_deploy": "aws cloudformation deploy --template-file app-output_sam.yaml --stack-name ${npm_package_name} --region eu-west-1 --capabilities CAPABILITY_IAM --parameter-override", 9 | "deploy": "npm run cf_package && npm run cf_deploy" 10 | }, 11 | "dependencies": { 12 | "@fastify/aws-lambda": "5.0.0", 13 | "fastify": "5.3.2", 14 | "i18next": "24.0.0", 15 | "i18next-fs-backend": "2.6.0", 16 | "i18next-http-middleware": "3.6.0", 17 | "mjml": "4.15.3", 18 | "@fastify/view": "10.0.1", 19 | "pug": "3.0.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/caching/app.js: -------------------------------------------------------------------------------- 1 | // serve translations 2 | const express = require('express') 3 | const app = express() 4 | app.use('/locales', express.static('locales')) 5 | app.listen(8080) 6 | 7 | // i18next in action... 8 | const i18next = require('i18next') 9 | const ChainedBackend = require('i18next-chained-backend') 10 | const FSBackend = require('i18next-fs-backend') 11 | // const FSBackend = require('../../cjs') 12 | const HttpBackend = require('i18next-http-backend') 13 | 14 | const config = { 15 | // debug: true, 16 | lng: 'en', 17 | fallbackLng: 'en', 18 | preload: ['en', 'de'], 19 | ns: ['translation'], 20 | defaultNS: 'translation', 21 | backend: { 22 | backends: [ 23 | FSBackend, 24 | HttpBackend 25 | ], 26 | backendOptions: [{ 27 | loadPath: './locales_cache/{{lng}}/{{ns}}.json', 28 | addPath: './locales_cache/{{lng}}/{{ns}}.json', 29 | expirationTime: 30 * 1000 // all 30 seconds the cache should be deleted 30 | }, { 31 | loadPath: 'http://localhost:8080/locales/{{lng}}/{{ns}}.json' 32 | }] 33 | } 34 | } 35 | 36 | setInterval(() => { 37 | const i18n = i18next.createInstance(config) 38 | i18n.use(ChainedBackend).init(config, (err, t) => { 39 | if (err) return console.error(err) 40 | console.log(t('welcome')) 41 | console.log(t('welcome', { lng: 'de' })) 42 | }) 43 | }, 3000) 44 | -------------------------------------------------------------------------------- /example/fastify/app.js: -------------------------------------------------------------------------------- 1 | import fastify from 'fastify' 2 | import pov from '@fastify/view' 3 | import pug from 'pug' 4 | import { register, i18next } from './lib/i18n.js' 5 | import mail from './lib/mail.js' 6 | 7 | const port = process.env.PORT || 8080 8 | 9 | const app = fastify() 10 | app.register(pov, { engine: { pug } }) 11 | register(app) 12 | 13 | app.get('/email', (request, reply) => { 14 | const html = mail('invitation', { 15 | title: request.t('invitation.subject'), 16 | t: request.t, 17 | firstname: request.query.name, 18 | invitationLink: 'https://locize.com' 19 | }) 20 | // would normally send this via some mail provider... 21 | reply.type('text/html') 22 | reply.send(html) 23 | }) 24 | 25 | app.get('/raw', (request, reply) => { 26 | reply.send(request.t('home.title')) 27 | }) 28 | 29 | app.get('/', (request, reply) => { 30 | reply.view('/views/index.pug') 31 | }) 32 | 33 | if (import.meta.url === `file://${process.argv[1]}`) { 34 | // called directly 35 | app.listen({ port }, (err) => { 36 | if (err) return console.error(err) 37 | console.log(i18next.t('server.started', { port })) 38 | console.log(i18next.t('server.started', { port, lng: 'de' })) 39 | console.log(i18next.t('server.started', { port, lng: 'it' })) 40 | }) 41 | } else { 42 | // imported as a module, i.e. when used in aws-lambda 43 | } 44 | 45 | export default app 46 | -------------------------------------------------------------------------------- /example/fastify/app-sam.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Transform": "AWS::Serverless-2016-10-31", 4 | "Parameters": { 5 | "Timeout": { 6 | "Type": "Number", 7 | "Default": 30000 8 | } 9 | }, 10 | "Resources": { 11 | "ExampleAppApi": { 12 | "Type": "AWS::Serverless::HttpApi", 13 | "Properties": { 14 | "StageName": "$default", 15 | "DefinitionBody": { 16 | "info": { 17 | "title": { 18 | "Ref": "AWS::StackName" 19 | } 20 | }, 21 | "openapi": "3.0.1", 22 | "paths": {} 23 | } 24 | } 25 | }, 26 | "ExampleApp": { 27 | "Type": "AWS::Serverless::Function", 28 | "Properties": { 29 | "FunctionName": { 30 | "Ref": "AWS::StackName" 31 | }, 32 | "Runtime": "nodejs14.x", 33 | "Handler": "lambda.handler", 34 | "Timeout": 30, 35 | "MemorySize": 512, 36 | "CodeUri": "build", 37 | "Events": { 38 | "ExampleAppProxyGET": { 39 | "Type": "HttpApi", 40 | "Properties": { 41 | "Path": "/{proxy+}", 42 | "Method": "GET", 43 | "ApiId": { 44 | "Ref": "ExampleAppApi" 45 | }, 46 | "TimeoutInMillis": { 47 | "Ref": "Timeout" 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /example/fallback/app.js: -------------------------------------------------------------------------------- 1 | // serve translations 2 | const express = require('express') 3 | const app = express() 4 | app.use('/locales', express.static('locales')) 5 | const server = app.listen(8080) 6 | 7 | // i18next in action... 8 | const i18next = require('i18next') 9 | const ChainedBackend = require('i18next-chained-backend') 10 | const FSBackend = require('i18next-fs-backend') 11 | // const FSBackend = require('../../cjs') 12 | const HttpBackend = require('i18next-http-backend') 13 | 14 | const initI18next = (cb) => { 15 | const i18n = i18next.createInstance() 16 | i18n.use(ChainedBackend).init({ 17 | // debug: true, 18 | lng: 'en', 19 | fallbackLng: 'en', 20 | preload: ['en', 'de'], 21 | ns: ['translation'], 22 | defaultNS: 'translation', 23 | backend: { 24 | backends: [ 25 | HttpBackend, 26 | FSBackend 27 | ], 28 | backendOptions: [{ 29 | loadPath: 'http://localhost:8080/locales/{{lng}}/{{ns}}.json' 30 | },{ 31 | loadPath: './locales_fallback/{{lng}}/{{ns}}.json' 32 | }] 33 | } 34 | }, cb) 35 | } 36 | 37 | initI18next((err, t) => { 38 | if (err) return console.error(err) 39 | console.log(t('welcome')) 40 | console.log(t('welcome', { lng: 'de' })) 41 | console.log('stopping http server...') 42 | server.close(() => { 43 | console.log('http server stopped') 44 | initI18next((err, t) => { 45 | if (err) return console.error(err) 46 | console.log(t('welcome')) 47 | console.log(t('welcome', { lng: 'de' })) 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { BackendModule, ReadCallback } from "i18next"; 2 | 3 | type LoadPathOption = 4 | | string 5 | | ((language: string, namespace: string) => string); 6 | 7 | type AddPathOption = 8 | | string 9 | | ((language: string, namespace: string) => string) 10 | 11 | export interface FsBackendOptions { 12 | /** 13 | * path where resources get loaded from, or a function 14 | * returning a path: 15 | * function(language, namespace) { return customPath; } 16 | * the returned path will interpolate lng, ns if provided like giving a static path 17 | */ 18 | loadPath?: LoadPathOption; 19 | /** 20 | * path to post missing resources, must be `string` or a `function` returning a path: 21 | * function(language, namespace) { return customPath; } 22 | */ 23 | addPath?: AddPathOption; 24 | ident?: number | undefined; 25 | parse?( 26 | data: string 27 | ): { [key: string]: any }; 28 | stringify?( 29 | data: { [key: string]: any } 30 | ): string; 31 | expirationTime?: number; 32 | } 33 | 34 | export default class I18NexFsBackend 35 | implements BackendModule 36 | { 37 | static type: "backend"; 38 | constructor(services?: any, options?: FsBackendOptions); 39 | init(services?: any, options?: FsBackendOptions): void; 40 | read(language: string, namespace: string, callback: ReadCallback): void; 41 | create?( 42 | languages: string[], 43 | namespace: string, 44 | key: string, 45 | fallbackValue: string 46 | ): void; 47 | type: "backend"; 48 | services: any; 49 | options: FsBackendOptions; 50 | } 51 | -------------------------------------------------------------------------------- /example/updatable-cache/app.js: -------------------------------------------------------------------------------- 1 | // serve translations 2 | const express = require('express') 3 | const app = express() 4 | app.use('/locales', express.static('locales')) 5 | app.listen(8080) 6 | 7 | // i18next in action... 8 | const i18next = require('i18next') 9 | const ChainedBackend = require('i18next-chained-backend') 10 | const FSBackend = require('i18next-fs-backend') 11 | // const FSBackend = require('../../cjs') 12 | const HttpBackend = require('i18next-http-backend') 13 | 14 | const config = { 15 | // debug: true, 16 | lng: 'en', 17 | fallbackLng: 'en', 18 | preload: ['en', 'de'], 19 | ns: ['translation'], 20 | defaultNS: 'translation', 21 | backend: { 22 | cacheHitMode: 'refreshAndUpdateStore', 23 | refreshExpirationTime: 30 * 1000, // only after 30 seconds it should trigger a refresh if necessary 24 | backends: [ 25 | FSBackend, 26 | HttpBackend 27 | ], 28 | backendOptions: [{ 29 | loadPath: './locales_cache/{{lng}}/{{ns}}.json', 30 | addPath: './locales_cache/{{lng}}/{{ns}}.json' 31 | }, { 32 | loadPath: 'http://localhost:8080/locales/{{lng}}/{{ns}}.json' 33 | }] 34 | } 35 | } 36 | 37 | i18next 38 | .use(ChainedBackend) 39 | .init(config, (err, t) => { 40 | if (err) return console.error(err) 41 | console.log(t('welcome')) 42 | console.log(t('welcome', { lng: 'de' })) 43 | 44 | setInterval(() => { 45 | i18next.reloadResources().then(() => { 46 | setImmediate(() => { 47 | console.log(t('welcome')) 48 | console.log(t('welcome', { lng: 'de' })) 49 | }) 50 | }) 51 | }, 3000) 52 | }) 53 | 54 | 55 | -------------------------------------------------------------------------------- /test/bun/backend.yaml.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, describe, beforeEach, afterEach } from "bun:test" 2 | import i18next from 'i18next' 3 | import Backend from '../../index.js' 4 | import { writeFile } from '../../lib/writeFile.js' 5 | 6 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 7 | 8 | describe('BackendConnector with yaml', () => { 9 | let connector 10 | 11 | beforeEach((done) => { 12 | connector = i18next.services.backendConnector 13 | connector.backend = new Backend(i18next.services, { 14 | loadPath: `${import.meta.dir}/../locales/{{lng}}/{{ns}}.yaml`, 15 | addPath: `${import.meta.dir}/../locales/{{lng}}/{{ns}}.yaml` 16 | }) 17 | writeFile(`${import.meta.dir}/../locales/en/test.yaml`, { key: 'passing' }).then(done).catch(done) 18 | }) 19 | 20 | afterEach((done) => { 21 | writeFile(`${import.meta.dir}/../locales/en/test.yaml`, { key: 'passing' }).then(done).catch(done) 22 | }) 23 | 24 | describe('#load', () => { 25 | test('should load data', (done) => { 26 | connector.load(['en'], ['test'], (err) => { 27 | expect(err).toBeFalsy() 28 | expect(connector.store.getResourceBundle('en', 'test')).toEqual({ 29 | key: 'passing' 30 | }) 31 | done() 32 | }) 33 | }) 34 | }) 35 | 36 | describe('#saveMissing', () => { 37 | test('should load data', (done) => { 38 | connector.backend.create(['en'], 'test', 'newKey', 'fallback', (err) => { 39 | expect(err).toBeFalsy() 40 | connector.backend.read(['en'], ['test'], (err, ns) => { 41 | expect(err).toBeFalsy() 42 | expect(ns).toEqual({ 43 | key: 'passing', 44 | newKey: 'fallback' 45 | }) 46 | done() 47 | }) 48 | }) 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /test/bun/backend.json.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, describe, beforeEach, afterEach } from "bun:test" 2 | import i18next from 'i18next' 3 | import Backend from '../../index.js' 4 | import { writeFile } from '../../lib/writeFile.js' 5 | 6 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 7 | 8 | describe('BackendConnector with normal json', () => { 9 | let connector 10 | 11 | beforeEach((done) => { 12 | connector = i18next.services.backendConnector 13 | connector.backend = new Backend(i18next.services, { 14 | loadPath: `${import.meta.dir}/../locales/{{lng}}/{{ns}}.json`, 15 | addPath: `${import.meta.dir}/../locales/{{lng}}/{{ns}}.json` 16 | }) 17 | writeFile(`${import.meta.dir}/../locales/en/test.json`, { key: 'passing' }).then(done).catch(done) 18 | }) 19 | 20 | afterEach((done) => { 21 | writeFile(`${import.meta.dir}/../locales/en/test.json`, { key: 'passing' }).then(done).catch(done) 22 | }) 23 | 24 | describe('#load', () => { 25 | test('should load data', (done) => { 26 | connector.load(['en'], ['test'], (err) => { 27 | expect(err).toBeFalsy() 28 | expect(connector.store.getResourceBundle('en', 'test')).toEqual({ 29 | key: 'passing' 30 | }) 31 | done() 32 | }) 33 | }) 34 | }) 35 | 36 | describe('#saveMissing', () => { 37 | test('should load data', (done) => { 38 | connector.backend.create(['en'], 'test', 'newKey', 'fallback', (err) => { 39 | expect(err).toBeFalsy() 40 | connector.backend.read(['en'], ['test'], (err, ns) => { 41 | expect(err).toBeFalsy() 42 | expect(ns).toEqual({ 43 | key: 'passing', 44 | newKey: 'fallback' 45 | }) 46 | done() 47 | }) 48 | }) 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /test/backend.yaml.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js' 2 | import { dirname } from 'path' 3 | import { fileURLToPath } from 'url' 4 | const __dirname = dirname(fileURLToPath(import.meta.url)) 5 | import i18next from 'i18next' 6 | import Backend from '../index.js' 7 | import { writeFile } from '../lib/writeFile.js' 8 | 9 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 10 | 11 | describe('BackendConnector with yaml', () => { 12 | let connector 13 | 14 | before((done) => { 15 | connector = i18next.services.backendConnector 16 | connector.backend = new Backend(i18next.services, { 17 | loadPath: `${__dirname}/locales/{{lng}}/{{ns}}.yaml`, 18 | addPath: `${__dirname}/locales/{{lng}}/{{ns}}.yaml` 19 | }) 20 | writeFile(`${__dirname}/locales/en/test.yaml`, { key: 'passing' }).then(done).catch(done) 21 | }) 22 | 23 | after((done) => { 24 | writeFile(`${__dirname}/locales/en/test.yaml`, { key: 'passing' }).then(done).catch(done) 25 | }) 26 | 27 | describe('#load', () => { 28 | it('should load data', (done) => { 29 | connector.load(['en'], ['test'], (err) => { 30 | expect(err).not.to.be.ok() 31 | expect(connector.store.getResourceBundle('en', 'test')).to.eql({ 32 | key: 'passing' 33 | }) 34 | done() 35 | }) 36 | }) 37 | }) 38 | 39 | describe('#saveMissing', () => { 40 | it('should load data', (done) => { 41 | connector.backend.create(['en'], 'test', 'newKey', 'fallback', (err) => { 42 | expect(err).not.to.be.ok() 43 | connector.backend.read(['en'], ['test'], (err, ns) => { 44 | expect(err).not.to.be.ok() 45 | expect(ns).to.eql({ 46 | key: 'passing', 47 | newKey: 'fallback' 48 | }) 49 | done() 50 | }) 51 | }) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/backend.json.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js' 2 | import { dirname } from 'path' 3 | import { fileURLToPath } from 'url' 4 | const __dirname = dirname(fileURLToPath(import.meta.url)) 5 | import i18next from 'i18next' 6 | import Backend from '../index.js' 7 | import { writeFile } from '../lib/writeFile.js' 8 | 9 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 10 | 11 | describe('BackendConnector with normal json', () => { 12 | let connector 13 | 14 | before((done) => { 15 | connector = i18next.services.backendConnector 16 | connector.backend = new Backend(i18next.services, { 17 | loadPath: `${__dirname}/locales/{{lng}}/{{ns}}.json`, 18 | addPath: `${__dirname}/locales/{{lng}}/{{ns}}.json` 19 | }) 20 | writeFile(`${__dirname}/locales/en/test.json`, { key: 'passing' }).then(done).catch(done) 21 | }) 22 | 23 | after((done) => { 24 | writeFile(`${__dirname}/locales/en/test.json`, { key: 'passing' }).then(done).catch(done) 25 | }) 26 | 27 | describe('#load', () => { 28 | it('should load data', (done) => { 29 | connector.load(['en'], ['test'], (err) => { 30 | expect(err).not.to.be.ok() 31 | expect(connector.store.getResourceBundle('en', 'test')).to.eql({ 32 | key: 'passing' 33 | }) 34 | done() 35 | }) 36 | }) 37 | }) 38 | 39 | describe('#saveMissing', () => { 40 | it('should load data', (done) => { 41 | connector.backend.create(['en'], 'test', 'newKey', 'fallback', (err) => { 42 | expect(err).not.to.be.ok() 43 | connector.backend.read(['en'], ['test'], (err, ns) => { 44 | expect(err).not.to.be.ok() 45 | expect(ns).to.eql({ 46 | key: 'passing', 47 | newKey: 'fallback' 48 | }) 49 | done() 50 | }) 51 | }) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/backend.js.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js' 2 | import { dirname } from 'path' 3 | import { fileURLToPath } from 'url' 4 | const __dirname = dirname(fileURLToPath(import.meta.url)) 5 | import i18next from 'i18next' 6 | import Backend from '../index.js' 7 | import { writeFile } from '../lib/writeFile.js' 8 | 9 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 10 | 11 | describe('BackendConnector with js', () => { 12 | let connector 13 | 14 | before((done) => { 15 | connector = i18next.services.backendConnector 16 | connector.backend = new Backend(i18next.services, { 17 | loadPath: `${__dirname}/locales/{{lng}}/{{ns}}.js`, 18 | addPath: `${__dirname}/locales/{{lng}}/{{ns}}.js` 19 | }) 20 | writeFile(`${__dirname}/locales/en/test.js`, { key: 'passing' }).then(done).catch(done) 21 | }) 22 | 23 | after((done) => { 24 | writeFile(`${__dirname}/locales/en/test.js`, { key: 'passing' }).then(done).catch(done) 25 | }) 26 | 27 | describe('#load', () => { 28 | it('should load data', (done) => { 29 | connector.load(['en'], ['test'], (err) => { 30 | expect(err).not.to.be.ok() 31 | expect(connector.store.getResourceBundle('en', 'test')).to.eql({ 32 | key: 'passing' 33 | }) 34 | done() 35 | }) 36 | }) 37 | }) 38 | 39 | describe('#saveMissing', () => { 40 | it('should load data', (done) => { 41 | connector.backend.create(['en'], 'test', 'newKey', 'fallback; of new key', (err) => { 42 | expect(err).not.to.be.ok() 43 | connector.backend.read(['en'], ['test'], (err, ns) => { 44 | expect(err).not.to.be.ok() 45 | expect(ns).to.eql({ 46 | key: 'passing', 47 | newKey: 'fallback; of new key' 48 | }) 49 | done() 50 | }) 51 | }) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/deno/backend.json.js: -------------------------------------------------------------------------------- 1 | const { test } = Deno 2 | import { assertEquals } from 'https://deno.land/std/testing/asserts.ts' 3 | import { fromFileUrl, dirname } from 'https://deno.land/std/path/mod.ts' 4 | const __dirname = dirname(fromFileUrl(import.meta.url)) 5 | import i18next from 'https://deno.land/x/i18next/index.js' 6 | import Backend from '../../index.js' 7 | import { writeFile } from '../../lib/writeFile.js' 8 | const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 9 | 10 | test('BackendConnector with normal json', async () => { 11 | // before 12 | await writeFile(`${__dirname}/../locales/en/test.json`, { key: 'passing' }) 13 | 14 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 15 | 16 | const connector = i18next.services.backendConnector 17 | connector.backend = new Backend(i18next.services, { 18 | loadPath: `${__dirname}/../locales/{{lng}}/{{ns}}.json`, 19 | addPath: `${__dirname}/../locales/{{lng}}/{{ns}}.json` 20 | }) 21 | 22 | // test 23 | await (new Promise((resolve, reject) => { 24 | connector.load(['en'], ['test'], (err) => err ? reject(err) : resolve()) 25 | })) 26 | 27 | assertEquals(connector.store.getResourceBundle('en', 'test'), { 28 | key: 'passing' 29 | }) 30 | 31 | await (new Promise((resolve, reject) => { 32 | connector.backend.create(['en'], 'test', 'newKey', 'fallback', (err) => err ? reject(err) : resolve()) 33 | })) 34 | 35 | const ns = await (new Promise((resolve, reject) => { 36 | connector.backend.read(['en'], ['test'], (err, ns) => err ? reject(err) : resolve(ns)) 37 | })) 38 | 39 | assertEquals(ns, { 40 | key: 'passing', 41 | newKey: 'fallback' 42 | }) 43 | 44 | // after 45 | await writeFile(`${__dirname}/../locales/en/test.json`, { key: 'passing' }) 46 | await wait(500) // I don't know why, probably because of debouncedWrite 47 | }) 48 | -------------------------------------------------------------------------------- /test/deno/backend.yaml.js: -------------------------------------------------------------------------------- 1 | const { test } = Deno 2 | import { assertEquals } from 'https://deno.land/std/testing/asserts.ts' 3 | import { fromFileUrl, dirname } from 'https://deno.land/std/path/mod.ts' 4 | const __dirname = dirname(fromFileUrl(import.meta.url)) 5 | import i18next from 'https://deno.land/x/i18next/index.js' 6 | import Backend from '../../index.js' 7 | import { writeFile } from '../../lib/writeFile.js' 8 | const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 9 | 10 | test('BackendConnector with yaml', async () => { 11 | // before 12 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 13 | 14 | const connector = i18next.services.backendConnector 15 | connector.backend = new Backend(i18next.services, { 16 | loadPath: `${__dirname}/../locales/{{lng}}/{{ns}}.yaml`, 17 | addPath: `${__dirname}/../locales/{{lng}}/{{ns}}.yaml` 18 | }) 19 | await wait(200) // I don't know why, probably because of debouncedWrite 20 | await writeFile(`${__dirname}/../locales/en/test.yaml`, { key: 'passing' }) 21 | 22 | // test 23 | await (new Promise((resolve, reject) => { 24 | connector.load(['en'], ['test'], (err) => err ? reject(err) : resolve()) 25 | })) 26 | 27 | assertEquals(connector.store.getResourceBundle('en', 'test'), { 28 | key: 'passing' 29 | }) 30 | 31 | await (new Promise((resolve, reject) => { 32 | connector.backend.create(['en'], 'test', 'newKey', 'fallback', (err) => err ? reject(err) : resolve()) 33 | })) 34 | 35 | const ns = await (new Promise((resolve, reject) => { 36 | connector.backend.read(['en'], ['test'], (err, ns) => err ? reject(err) : resolve(ns)) 37 | })) 38 | 39 | assertEquals(ns, { 40 | key: 'passing', 41 | newKey: 'fallback' 42 | }) 43 | 44 | // after 45 | await writeFile(`${__dirname}/../locales/en/test.yaml`, { key: 'passing' }) 46 | await wait(500) // I don't know why, probably because of debouncedWrite 47 | }) 48 | -------------------------------------------------------------------------------- /test/bun/backend.caching.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, describe, beforeEach, afterEach } from "bun:test" 2 | import i18next from 'i18next' 3 | import Backend from '../../index.js' 4 | import { removeFile } from '../../lib/writeFile.js' 5 | 6 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 7 | 8 | describe('BackendConnector as caching layer', () => { 9 | let connector 10 | 11 | beforeEach((done) => { 12 | connector = i18next.services.backendConnector 13 | connector.backend = new Backend(i18next.services, { 14 | loadPath: `${__dirname}/locales/{{lng}}/{{ns}}.json`, 15 | addPath: `${__dirname}/locales/{{lng}}/{{ns}}.json`, 16 | expirationTime: 250 17 | }) 18 | removeFile(`${__dirname}/locales/de/test_caching.json`).then(done).catch(() => done()) 19 | }) 20 | 21 | afterEach((done) => { 22 | removeFile(`${__dirname}/locales/de/test_caching.json`).then(done).catch(() => done()) 23 | }) 24 | 25 | describe('caching szenario', () => { 26 | test('should work as expected', (done) => { 27 | connector.backend.read(['de'], ['test_caching'], (err, ns) => { 28 | expect(err).toBeTruthy() 29 | 30 | connector.backend.save('de', 'test_caching', { key: 'save in cache' }, (err) => { 31 | expect(err).toBeFalsy() 32 | 33 | connector.backend.read(['de'], ['test_caching'], (err, ns) => { 34 | expect(err).toBeFalsy() 35 | expect(ns).toEqual({ 36 | key: 'save in cache' 37 | }) 38 | 39 | setTimeout(() => { 40 | connector.backend.read(['de'], ['test_caching'], (err, ns) => { 41 | try { 42 | expect(err).toBeTruthy() 43 | done() 44 | } catch (e) { 45 | done(e) 46 | } 47 | }) 48 | }, 300) 49 | }) 50 | }) 51 | }) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/bun/backend.js.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, describe, beforeEach, afterEach } from "bun:test" 2 | import i18next from 'i18next' 3 | import Backend from '../../index.js' 4 | import { writeFile } from '../../lib/writeFile.js' 5 | 6 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 7 | 8 | describe.skip('BackendConnector with js', () => { 9 | let connector 10 | 11 | beforeEach((done) => { 12 | connector = i18next.services.backendConnector 13 | connector.backend = new Backend(i18next.services, { 14 | loadPath: `${import.meta.dir}/../locales/{{lng}}/{{ns}}.js`, 15 | addPath: `${import.meta.dir}/../locales/{{lng}}/{{ns}}.js` 16 | }) 17 | writeFile(`${import.meta.dir}/../locales/en/test.js`, { key: 'passing' }).then(done).catch(done) 18 | }) 19 | 20 | afterEach((done) => { 21 | writeFile(`${import.meta.dir}/../locales/en/test.js`, { key: 'passing' }).then(done).catch(done) 22 | }) 23 | 24 | describe('#load', () => { 25 | test('should load data', (done) => { 26 | connector.load(['en'], ['test'], (err) => { 27 | expect(err).toBeFalsy() 28 | console.log(3) 29 | console.log(connector.store.getResourceBundle('en', 'test')) 30 | expect(connector.store.getResourceBundle('en', 'test')).toEqual({ 31 | key: 'passing' 32 | }) 33 | console.log(4) 34 | done() 35 | }) 36 | }) 37 | }) 38 | 39 | describe('#saveMissing', () => { 40 | test('should load data', (done) => { 41 | connector.backend.create(['en'], 'test', 'newKey', 'fallback; of new key', (err) => { 42 | expect(err).toBeFalsy() 43 | connector.backend.read(['en'], ['test'], (err, ns) => { 44 | expect(err).toBeFalsy() 45 | expect(ns).toEqual({ 46 | key: 'passing', 47 | newKey: 'fallback; of new key' 48 | }) 49 | done() 50 | }) 51 | }) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /test/deno/backend.js.js: -------------------------------------------------------------------------------- 1 | const { test } = Deno 2 | import { assertEquals } from 'https://deno.land/std/testing/asserts.ts' 3 | import { fromFileUrl, dirname } from 'https://deno.land/std/path/mod.ts' 4 | const __dirname = dirname(fromFileUrl(import.meta.url)) 5 | import i18next from 'https://deno.land/x/i18next/index.js' 6 | import Backend from '../../index.js' 7 | import { writeFile } from '../../lib/writeFile.js' 8 | const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 9 | 10 | test('BackendConnector with js', async () => { 11 | // before 12 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 13 | 14 | const connector = i18next.services.backendConnector 15 | connector.backend = new Backend(i18next.services, { 16 | loadPath: `${__dirname}/../locales/{{lng}}/{{ns}}.js`, 17 | addPath: `${__dirname}/../locales/{{lng}}/{{ns}}.js` 18 | }) 19 | await wait(200) // I don't know why, probably because of debouncedWrite 20 | await writeFile(`${__dirname}/../locales/en/test.js`, { key: 'passing' }) 21 | 22 | // test 23 | await (new Promise((resolve, reject) => { 24 | connector.load(['en'], ['test'], (err) => err ? reject(err) : resolve()) 25 | })) 26 | 27 | assertEquals(connector.store.getResourceBundle('en', 'test'), { 28 | key: 'passing' 29 | }) 30 | 31 | await (new Promise((resolve, reject) => { 32 | connector.backend.create(['en'], 'test', 'newKey', 'fallback; of new key', (err) => err ? reject(err) : resolve()) 33 | })) 34 | 35 | const ns = await (new Promise((resolve, reject) => { 36 | connector.backend.read(['en'], ['test'], (err, ns) => err ? reject(err) : resolve(ns)) 37 | })) 38 | 39 | assertEquals(ns, { 40 | key: 'passing', 41 | newKey: 'fallback; of new key' 42 | }) 43 | 44 | // after 45 | await writeFile(`${__dirname}/../locales/en/test.js`, { key: 'passing' }) 46 | await wait(500) // I don't know why, probably because of debouncedWrite 47 | }) 48 | -------------------------------------------------------------------------------- /test/backend.caching.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js' 2 | import { dirname } from 'path' 3 | import { fileURLToPath } from 'url' 4 | const __dirname = dirname(fileURLToPath(import.meta.url)) 5 | import i18next from 'i18next' 6 | import Backend from '../index.js' 7 | import { removeFile } from '../lib/writeFile.js' 8 | 9 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 10 | 11 | describe('BackendConnector as caching layer', () => { 12 | let connector 13 | 14 | before((done) => { 15 | connector = i18next.services.backendConnector 16 | connector.backend = new Backend(i18next.services, { 17 | loadPath: `${__dirname}/locales/{{lng}}/{{ns}}.json`, 18 | addPath: `${__dirname}/locales/{{lng}}/{{ns}}.json`, 19 | expirationTime: 250 20 | }) 21 | removeFile(`${__dirname}/locales/de/test_caching.json`).then(done).catch(() => done()) 22 | }) 23 | 24 | after((done) => { 25 | removeFile(`${__dirname}/locales/de/test_caching.json`).then(done).catch(() => done()) 26 | }) 27 | 28 | describe('caching szenario', () => { 29 | it('should work as expected', (done) => { 30 | connector.backend.read(['de'], ['test_caching'], (err, ns) => { 31 | expect(err).to.be.ok() 32 | 33 | connector.backend.save('de', 'test_caching', { key: 'save in cache' }, (err) => { 34 | expect(err).not.to.be.ok() 35 | 36 | connector.backend.read(['de'], ['test_caching'], (err, ns) => { 37 | expect(err).not.to.be.ok() 38 | expect(ns).to.eql({ 39 | key: 'save in cache' 40 | }) 41 | 42 | setTimeout(() => { 43 | connector.backend.read(['de'], ['test_caching'], (err, ns) => { 44 | try { 45 | expect(err).to.be.ok() 46 | done() 47 | } catch (e) { 48 | done(e) 49 | } 50 | }) 51 | }, 300) 52 | }) 53 | }) 54 | }) 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /example/fastify/mails/layout.pug: -------------------------------------------------------------------------------- 1 | mjml 2 | mj-body(background-color='#ffffff') 3 | mj-section(background-color='#ffffff', padding-bottom='0px', padding-top='0') 4 | mj-column(vertical-align='top', width='100%') 5 | block headerImage 6 | - var imgUrl = imageUrl || 'https://raw.githubusercontent.com/i18next/i18next/master/assets/i18next-ecosystem.jpg' 7 | mj-image(src=imgUrl, alt='', align='center', border='none', width='600px', padding-left='0px', padding-right='0px', padding-bottom='0px', padding-top='0') 8 | mj-section(background-color='#024b3f', padding-bottom='0px', padding-top='0') 9 | mj-column(vertical-align='top', width='100%') 10 | block subject 11 | mj-text(align='left', color='#ffffff', font-size='22px', font-weight='bold', font-family='open Sans Helvetica, Arial, sans-serif', padding-left='25px', padding-right='25px', padding-bottom='30px', padding-top='50px')=title || 'Mail' 12 | mj-section(background-color='#26a69a', padding-bottom='20px', padding-top='20px') 13 | mj-column(vertical-align='middle', width='100%') 14 | block greeting 15 | mj-text(align='left', color='#ffffff', font-size='16px', font-family='open Sans Helvetica, Arial, sans-serif', padding-left='25px', padding-right='25px') 16 | span(style='color:#024b3f')=t('invitation.greeting', { firstname: firstname || 'Test' }) 17 | br 18 | br 19 | block content 20 | mj-text(align='left', color='#ffffff', font-size='16px', font-family='open Sans Helvetica, Arial, sans-serif', padding-left='25px', padding-right='25px') 21 | br 22 | block salutaion 23 | mj-text(align='left', color='#ffffff', font-size='16px', font-family='open Sans Helvetica, Arial, sans-serif', padding-left='25px', padding-right='25px') 24 | span=t('invitation.salutation') 25 | br 26 | a(href='https://www.i18next.com', style='text-decoration: none; color: #ffffff') www.i18next.com 27 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | const arr = [] 2 | const each = arr.forEach 3 | const slice = arr.slice 4 | 5 | export function defaults (obj) { 6 | each.call(slice.call(arguments, 1), (source) => { 7 | if (source) { 8 | for (const prop in source) { 9 | if (obj[prop] === undefined) obj[prop] = source[prop] 10 | } 11 | } 12 | }) 13 | return obj 14 | } 15 | 16 | export function debounce (func, wait, immediate) { 17 | let timeout 18 | return function () { 19 | const context = this 20 | const args = arguments 21 | const later = function () { 22 | timeout = null 23 | if (!immediate) func.apply(context, args) 24 | } 25 | const callNow = immediate && !timeout 26 | clearTimeout(timeout) 27 | timeout = setTimeout(later, wait) 28 | if (callNow) func.apply(context, args) 29 | } 30 | } 31 | 32 | function getLastOfPath (object, path, Empty) { 33 | function cleanKey (key) { 34 | return (key && key.indexOf('###') > -1) ? key.replace(/###/g, '.') : key 35 | } 36 | 37 | const stack = (typeof path !== 'string') ? [].concat(path) : path.split('.') 38 | while (stack.length > 1) { 39 | if (!object) return {} 40 | 41 | const key = cleanKey(stack.shift()) 42 | if (!object[key] && Empty) object[key] = new Empty() 43 | object = object[key] 44 | } 45 | 46 | if (!object) return {} 47 | return { 48 | obj: object, 49 | k: cleanKey(stack.shift()) 50 | } 51 | } 52 | 53 | export function setPath (object, path, newValue) { 54 | const { obj, k } = getLastOfPath(object, path, Object) 55 | if (Array.isArray(obj) && isNaN(k)) throw new Error(`Cannot create property "${k}" here since object is an array`) 56 | obj[k] = newValue 57 | } 58 | 59 | export function pushPath (object, path, newValue, concat) { 60 | const { obj, k } = getLastOfPath(object, path, Object) 61 | obj[k] = obj[k] || [] 62 | if (concat) obj[k] = obj[k].concat(newValue) 63 | if (!concat) obj[k].push(newValue) 64 | } 65 | 66 | export function getPath (object, path) { 67 | const { obj, k } = getLastOfPath(object, path) 68 | if (!obj) return undefined 69 | return obj[k] 70 | } 71 | -------------------------------------------------------------------------------- /test/deno/backend.caching.js: -------------------------------------------------------------------------------- 1 | const { test } = Deno 2 | import { assertEquals } from 'https://deno.land/std/testing/asserts.ts' 3 | import { fromFileUrl, dirname } from 'https://deno.land/std/path/mod.ts' 4 | const __dirname = dirname(fromFileUrl(import.meta.url)) 5 | import i18next from 'https://deno.land/x/i18next/index.js' 6 | import Backend from '../../index.js' 7 | import { removeFile } from '../../lib/writeFile.js' 8 | const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 9 | 10 | test('BackendConnector as caching layer', async () => { 11 | // before 12 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 13 | 14 | const connector = i18next.services.backendConnector 15 | connector.backend = new Backend(i18next.services, { 16 | loadPath: `${__dirname}/../locales/{{lng}}/{{ns}}.json`, 17 | addPath: `${__dirname}/../locales/{{lng}}/{{ns}}.json`, 18 | expirationTime: 250 19 | }) 20 | await wait(200) // I don't know why, probably because of debouncedWrite 21 | try { 22 | await removeFile(`${__dirname}/../locales/de/test_caching.json`) 23 | } catch (e) {} 24 | 25 | // test 26 | await (new Promise((resolve, reject) => { 27 | connector.backend.read(['de'], ['test_caching'], (err) => { 28 | if (!err) return reject(new Error('An error is expected here!')) 29 | resolve() 30 | }) 31 | })) 32 | 33 | await (new Promise((resolve, reject) => { 34 | connector.backend.save('de', 'test_caching', { key: 'save in cache' }, (err) => { 35 | if (err) return reject(err) 36 | resolve() 37 | }) 38 | })) 39 | 40 | const ns = await (new Promise((resolve, reject) => { 41 | connector.backend.read(['de'], ['test_caching'], (err, ns) => err ? reject(err) : resolve(ns)) 42 | })) 43 | 44 | assertEquals(ns, { 45 | key: 'save in cache' 46 | }) 47 | 48 | await wait(300) 49 | 50 | await (new Promise((resolve, reject) => { 51 | connector.backend.read(['de'], ['test_caching'], (err) => { 52 | if (!err) return reject(new Error('An error is expected here!')) 53 | resolve() 54 | }) 55 | })) 56 | 57 | // after 58 | try { 59 | await removeFile(`${__dirname}/../locales/de/test_caching.json`) 60 | } catch (e) {} 61 | await wait(500) // I don't know why, probably because of debouncedWrite 62 | }) 63 | -------------------------------------------------------------------------------- /test/deno/backend.json5.js: -------------------------------------------------------------------------------- 1 | const { test, writeTextFile } = Deno 2 | import { assertEquals } from 'https://deno.land/std/testing/asserts.ts' 3 | import { fromFileUrl, dirname } from 'https://deno.land/std/path/mod.ts' 4 | const __dirname = dirname(fromFileUrl(import.meta.url)) 5 | import i18next from 'https://deno.land/x/i18next/index.js' 6 | import Backend from '../../index.js' 7 | import { writeFile } from '../../lib/writeFile.js' 8 | const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 9 | 10 | test('BackendConnector with json5', async () => { 11 | // before 12 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 13 | 14 | const connector = i18next.services.backendConnector 15 | connector.backend = new Backend(i18next.services, { 16 | loadPath: `${__dirname}/../locales/{{lng}}/{{ns}}.json5`, 17 | addPath: `${__dirname}/../locales/{{lng}}/{{ns}}.json5` 18 | }) 19 | await wait(200) // I don't know why, probably because of debouncedWrite 20 | await writeFile(`${__dirname}/../locales/en/test.json5`, { key: 'passing' }) 21 | await writeTextFile(`${__dirname}/../locales/en/test-with-comments.json5`, `{ 22 | "key": "passing", 23 | // line comment 24 | "commented": "value", /* inline block */ 25 | /* block comment 26 | multiple lines */ 27 | "block": "value" 28 | }`) 29 | 30 | // test 31 | await (new Promise((resolve, reject) => { 32 | connector.load(['en'], ['test'], (err) => err ? reject(err) : resolve()) 33 | })) 34 | 35 | assertEquals(connector.store.getResourceBundle('en', 'test'), { 36 | key: 'passing' 37 | }) 38 | 39 | await (new Promise((resolve, reject) => { 40 | connector.load(['en'], ['test-with-comments'], (err) => err ? reject(err) : resolve()) 41 | })) 42 | 43 | assertEquals(connector.store.getResourceBundle('en', 'test-with-comments'), { 44 | key: 'passing', 45 | commented: 'value', 46 | block: 'value' 47 | }) 48 | 49 | await (new Promise((resolve, reject) => { 50 | connector.backend.create(['en'], 'test', 'newKey', 'fallback', (err) => err ? reject(err) : resolve()) 51 | })) 52 | 53 | const ns = await (new Promise((resolve, reject) => { 54 | connector.backend.read(['en'], ['test'], (err, ns) => err ? reject(err) : resolve(ns)) 55 | })) 56 | 57 | assertEquals(ns, { 58 | key: 'passing', 59 | newKey: 'fallback' 60 | }) 61 | 62 | // after 63 | await writeFile(`${__dirname}/../locales/en/test.json5`, { key: 'passing' }) 64 | await wait(500) // I don't know why, probably because of debouncedWrite 65 | }) 66 | -------------------------------------------------------------------------------- /test/deno/backend.jsonc.js: -------------------------------------------------------------------------------- 1 | const { test, writeTextFile } = Deno 2 | import { assertEquals } from 'https://deno.land/std/testing/asserts.ts' 3 | import { fromFileUrl, dirname } from 'https://deno.land/std/path/mod.ts' 4 | const __dirname = dirname(fromFileUrl(import.meta.url)) 5 | import i18next from 'https://deno.land/x/i18next/index.js' 6 | import Backend from '../../index.js' 7 | import { writeFile } from '../../lib/writeFile.js' 8 | const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) 9 | 10 | test('BackendConnector with jsonc', async () => { 11 | // before 12 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 13 | 14 | const connector = i18next.services.backendConnector 15 | connector.backend = new Backend(i18next.services, { 16 | loadPath: `${__dirname}/../locales/{{lng}}/{{ns}}.jsonc`, 17 | addPath: `${__dirname}/../locales/{{lng}}/{{ns}}.jsonc` 18 | }) 19 | await wait(200) // I don't know why, probably because of debouncedWrite 20 | await writeFile(`${__dirname}/../locales/en/test.jsonc`, { key: 'passing' }) 21 | await writeTextFile(`${__dirname}/../locales/en/test-with-comments.jsonc`, `{ 22 | "key": "passing", 23 | // line comment 24 | "commented": "value", /* inline block */ 25 | /* block comment 26 | multiple lines */ 27 | "block": "value" 28 | }`) 29 | 30 | // test 31 | await (new Promise((resolve, reject) => { 32 | connector.load(['en'], ['test'], (err) => err ? reject(err) : resolve()) 33 | })) 34 | 35 | assertEquals(connector.store.getResourceBundle('en', 'test'), { 36 | key: 'passing' 37 | }) 38 | 39 | await (new Promise((resolve, reject) => { 40 | connector.load(['en'], ['test-with-comments'], (err) => err ? reject(err) : resolve()) 41 | })) 42 | 43 | assertEquals(connector.store.getResourceBundle('en', 'test-with-comments'), { 44 | key: 'passing', 45 | commented: 'value', 46 | block: 'value' 47 | }) 48 | 49 | await (new Promise((resolve, reject) => { 50 | connector.backend.create(['en'], 'test', 'newKey', 'fallback', (err) => err ? reject(err) : resolve()) 51 | })) 52 | 53 | const ns = await (new Promise((resolve, reject) => { 54 | connector.backend.read(['en'], ['test'], (err, ns) => err ? reject(err) : resolve(ns)) 55 | })) 56 | 57 | assertEquals(ns, { 58 | key: 'passing', 59 | newKey: 'fallback' 60 | }) 61 | 62 | // after 63 | await writeFile(`${__dirname}/../locales/en/test.jsonc`, { key: 'passing' }) 64 | await writeTextFile(`${__dirname}/../locales/en/test-with-comments.jsonc`, `{ 65 | "key": "passing", 66 | // line comment 67 | "commented": "value", /* inline block */ 68 | /* block comment 69 | multiple lines */ 70 | "block": "value" 71 | }`) 72 | await wait(500) // I don't know why, probably because of debouncedWrite 73 | }) 74 | -------------------------------------------------------------------------------- /test/bun/backend.json5.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, describe, beforeEach, afterEach } from "bun:test" 2 | import i18next from 'i18next' 3 | import Backend from '../../index.js' 4 | import { writeFile } from '../../lib/writeFile.js' 5 | 6 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 7 | 8 | describe('BackendConnector with json5', () => { 9 | let connector 10 | 11 | beforeEach((done) => { 12 | connector = i18next.services.backendConnector 13 | connector.backend = new Backend(i18next.services, { 14 | loadPath: `${import.meta.dir}/../locales/{{lng}}/{{ns}}.json5`, 15 | addPath: `${import.meta.dir}/../locales/{{lng}}/{{ns}}.json5` 16 | }) 17 | writeFile(`${import.meta.dir}/../locales/en/test.json5`, { key: 'passing' }) 18 | .then(() => { 19 | Bun.write(`${import.meta.dir}/../locales/en/test-with-comments.json5`, `{ 20 | "key": "passing", 21 | // line comment 22 | "commented": "value", /* inline block */ 23 | /* block comment 24 | multiple lines */ 25 | "block": "value" 26 | }`) 27 | done() 28 | }) 29 | .catch(done) 30 | }) 31 | 32 | afterEach((done) => { 33 | writeFile(`${import.meta.dir}/../locales/en/test.json5`, { key: 'passing' }) 34 | .then(() => { 35 | Bun.write(`${import.meta.dir}/../locales/en/test-with-comments.json5`, `{ 36 | "key": "passing", 37 | // line comment 38 | "commented": "value", /* inline block */ 39 | /* block comment 40 | multiple lines */ 41 | "block": "value" 42 | }`) 43 | done() 44 | }) 45 | .catch(done) 46 | }) 47 | 48 | describe('#load', () => { 49 | test('should load data', (done) => { 50 | connector.load(['en'], ['test'], (err) => { 51 | expect(err).toBeFalsy() 52 | expect(connector.store.getResourceBundle('en', 'test')).toEqual({ 53 | key: 'passing' 54 | }) 55 | done() 56 | }) 57 | }) 58 | 59 | test('should load data with comments', (done) => { 60 | connector.load(['en'], ['test-with-comments'], (err) => { 61 | expect(err).toBeFalsy() 62 | expect(connector.store.getResourceBundle('en', 'test-with-comments')).toEqual({ 63 | key: 'passing', 64 | commented: 'value', 65 | block: 'value' 66 | }) 67 | done() 68 | }) 69 | }) 70 | }) 71 | 72 | describe('#saveMissing', () => { 73 | test('should load data', (done) => { 74 | connector.backend.create(['en'], 'test', 'newKey', 'fallback', (err) => { 75 | expect(err).toBeFalsy() 76 | connector.backend.read(['en'], ['test'], (err, ns) => { 77 | expect(err).toBeFalsy() 78 | expect(ns).toEqual({ 79 | key: 'passing', 80 | newKey: 'fallback' 81 | }) 82 | done() 83 | }) 84 | }) 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /test/bun/backend.jsonc.test.js: -------------------------------------------------------------------------------- 1 | import { expect, test, describe, beforeEach, afterEach } from "bun:test" 2 | import i18next from 'i18next' 3 | import Backend from '../../index.js' 4 | import { writeFile } from '../../lib/writeFile.js' 5 | 6 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 7 | 8 | describe('BackendConnector with jsonc', () => { 9 | let connector 10 | 11 | beforeEach((done) => { 12 | connector = i18next.services.backendConnector 13 | connector.backend = new Backend(i18next.services, { 14 | loadPath: `${import.meta.dir}/../locales/{{lng}}/{{ns}}.jsonc`, 15 | addPath: `${import.meta.dir}/../locales/{{lng}}/{{ns}}.jsonc` 16 | }) 17 | writeFile(`${import.meta.dir}/../locales/en/test.jsonc`, { key: 'passing' }) 18 | .then(() => { 19 | Bun.write(`${import.meta.dir}/../locales/en/test-with-comments.jsonc`, `{ 20 | "key": "passing", 21 | // line comment 22 | "commented": "value", /* inline block */ 23 | /* block comment 24 | multiple lines */ 25 | "block": "value" 26 | }`) 27 | done() 28 | }) 29 | .catch(done) 30 | }) 31 | 32 | afterEach((done) => { 33 | writeFile(`${import.meta.dir}/../locales/en/test.jsonc`, { key: 'passing' }) 34 | .then(() => { 35 | Bun.write(`${import.meta.dir}/../locales/en/test-with-comments.jsonc`, `{ 36 | "key": "passing", 37 | // line comment 38 | "commented": "value", /* inline block */ 39 | /* block comment 40 | multiple lines */ 41 | "block": "value" 42 | }`) 43 | done() 44 | }) 45 | .catch(done) 46 | }) 47 | 48 | describe('#load', () => { 49 | test('should load data', (done) => { 50 | connector.load(['en'], ['test'], (err) => { 51 | expect(err).toBeFalsy() 52 | expect(connector.store.getResourceBundle('en', 'test')).toEqual({ 53 | key: 'passing' 54 | }) 55 | done() 56 | }) 57 | }) 58 | 59 | test('should load data with comments', (done) => { 60 | connector.load(['en'], ['test-with-comments'], (err) => { 61 | expect(err).toBeFalsy() 62 | expect(connector.store.getResourceBundle('en', 'test-with-comments')).toEqual({ 63 | key: 'passing', 64 | commented: 'value', 65 | block: 'value' 66 | }) 67 | done() 68 | }) 69 | }) 70 | }) 71 | 72 | describe('#saveMissing', () => { 73 | test('should load data', (done) => { 74 | connector.backend.create(['en'], 'test', 'newKey', 'fallback', (err) => { 75 | expect(err).toBeFalsy() 76 | connector.backend.read(['en'], ['test'], (err, ns) => { 77 | expect(err).toBeFalsy() 78 | expect(ns).toEqual({ 79 | key: 'passing', 80 | newKey: 'fallback' 81 | }) 82 | done() 83 | }) 84 | }) 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /test/backend.json5.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js' 2 | import { dirname } from 'path' 3 | import { fileURLToPath } from 'url' 4 | import fs from 'fs' 5 | const __dirname = dirname(fileURLToPath(import.meta.url)) 6 | import i18next from 'i18next' 7 | import Backend from '../index.js' 8 | import { writeFile } from '../lib/writeFile.js' 9 | 10 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 11 | 12 | describe('BackendConnector with json5', () => { 13 | let connector 14 | 15 | before((done) => { 16 | connector = i18next.services.backendConnector 17 | connector.backend = new Backend(i18next.services, { 18 | loadPath: `${__dirname}/locales/{{lng}}/{{ns}}.json5`, 19 | addPath: `${__dirname}/locales/{{lng}}/{{ns}}.json5` 20 | }) 21 | writeFile(`${__dirname}/locales/en/test.json5`, { key: 'passing' }) 22 | .then(() => { 23 | fs.writeFile(`${__dirname}/locales/en/test-with-comments.json5`, `{ 24 | "key": "passing", 25 | // line comment 26 | "commented": "value", /* inline block */ 27 | /* block comment 28 | multiple lines */ 29 | "block": "value" 30 | }`, done) 31 | }) 32 | .catch(done) 33 | }) 34 | 35 | after((done) => { 36 | writeFile(`${__dirname}/locales/en/test.json5`, { key: 'passing' }) 37 | .then(() => { 38 | fs.writeFile(`${__dirname}/locales/en/test-with-comments.json5`, `{ 39 | "key": "passing", 40 | // line comment 41 | "commented": "value", /* inline block */ 42 | /* block comment 43 | multiple lines */ 44 | "block": "value" 45 | }`, done) 46 | }) 47 | .catch(done) 48 | }) 49 | 50 | describe('#load', () => { 51 | it('should load data', (done) => { 52 | connector.load(['en'], ['test'], (err) => { 53 | expect(err).not.to.be.ok() 54 | expect(connector.store.getResourceBundle('en', 'test')).to.eql({ 55 | key: 'passing' 56 | }) 57 | done() 58 | }) 59 | }) 60 | 61 | it('should load data with comments', (done) => { 62 | connector.load(['en'], ['test-with-comments'], (err) => { 63 | expect(err).not.to.be.ok() 64 | expect(connector.store.getResourceBundle('en', 'test-with-comments')).to.eql({ 65 | key: 'passing', 66 | commented: 'value', 67 | block: 'value' 68 | }) 69 | done() 70 | }) 71 | }) 72 | }) 73 | 74 | describe('#saveMissing', () => { 75 | it('should load data', (done) => { 76 | connector.backend.create(['en'], 'test', 'newKey', 'fallback', (err) => { 77 | expect(err).not.to.be.ok() 78 | connector.backend.read(['en'], ['test'], (err, ns) => { 79 | expect(err).not.to.be.ok() 80 | expect(ns).to.eql({ 81 | key: 'passing', 82 | newKey: 'fallback' 83 | }) 84 | done() 85 | }) 86 | }) 87 | }) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /test/backend.jsonc.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect.js' 2 | import { dirname } from 'path' 3 | import { fileURLToPath } from 'url' 4 | import fs from 'node:fs' 5 | const __dirname = dirname(fileURLToPath(import.meta.url)) 6 | import i18next from 'i18next' 7 | import Backend from '../index.js' 8 | import { writeFile } from '../lib/writeFile.js' 9 | 10 | i18next.init({ fallbackLng: 'en', ns: 'test' }) 11 | 12 | describe('BackendConnector with jsonc', () => { 13 | let connector 14 | 15 | before((done) => { 16 | connector = i18next.services.backendConnector 17 | connector.backend = new Backend(i18next.services, { 18 | loadPath: `${__dirname}/locales/{{lng}}/{{ns}}.jsonc`, 19 | addPath: `${__dirname}/locales/{{lng}}/{{ns}}.jsonc` 20 | }) 21 | writeFile(`${__dirname}/locales/en/test.jsonc`, { key: 'passing' }) 22 | .then(() => { 23 | fs.writeFile(`${__dirname}/locales/en/test-with-comments.jsonc`, `{ 24 | "key": "passing", 25 | // line comment 26 | "commented": "value", /* inline block */ 27 | /* block comment 28 | multiple lines */ 29 | "block": "value" 30 | }`, done) 31 | }) 32 | .catch(done) 33 | }) 34 | 35 | after((done) => { 36 | writeFile(`${__dirname}/locales/en/test.jsonc`, { key: 'passing' }) 37 | .then(() => { 38 | fs.writeFile(`${__dirname}/locales/en/test-with-comments.jsonc`, `{ 39 | "key": "passing", 40 | // line comment 41 | "commented": "value", /* inline block */ 42 | /* block comment 43 | multiple lines */ 44 | "block": "value" 45 | }`, done) 46 | }) 47 | .catch(done) 48 | }) 49 | 50 | describe('#load', () => { 51 | it('should load data', (done) => { 52 | connector.load(['en'], ['test'], (err) => { 53 | expect(err).not.to.be.ok() 54 | expect(connector.store.getResourceBundle('en', 'test')).to.eql({ 55 | key: 'passing' 56 | }) 57 | done() 58 | }) 59 | }) 60 | 61 | it('should load data with comments', (done) => { 62 | connector.load(['en'], ['test-with-comments'], (err) => { 63 | expect(err).not.to.be.ok() 64 | expect(connector.store.getResourceBundle('en', 'test-with-comments')).to.eql({ 65 | key: 'passing', 66 | commented: 'value', 67 | block: 'value' 68 | }) 69 | done() 70 | }) 71 | }) 72 | }) 73 | 74 | describe('#saveMissing', () => { 75 | it('should load data', (done) => { 76 | connector.backend.create(['en'], 'test', 'newKey', 'fallback', (err) => { 77 | expect(err).not.to.be.ok() 78 | connector.backend.read(['en'], ['test'], (err, ns) => { 79 | expect(err).not.to.be.ok() 80 | expect(ns).to.eql({ 81 | key: 'passing', 82 | newKey: 'fallback' 83 | }) 84 | done() 85 | }) 86 | }) 87 | }) 88 | }) 89 | }) 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "i18next-fs-backend", 3 | "version": "2.6.1", 4 | "private": false, 5 | "type": "module", 6 | "main": "./cjs/index.js", 7 | "exports": { 8 | "./package.json": "./package.json", 9 | ".": { 10 | "types": { 11 | "require": "./cjs/index.d.ts", 12 | "import": "./esm/index.d.mts" 13 | }, 14 | "module": "./esm/index.js", 15 | "import": "./esm/index.js", 16 | "require": "./cjs/index.js", 17 | "default": "./esm/index.js" 18 | }, 19 | "./cjs": { 20 | "types": "./cjs/index.d.ts", 21 | "default": "./cjs/index.js" 22 | }, 23 | "./esm": { 24 | "types": "./esm/index.d.mts", 25 | "default": "./esm/index.js" 26 | } 27 | }, 28 | "module": "./esm/index.js", 29 | "types": "./index.d.mts", 30 | "devDependencies": { 31 | "@babel/cli": "7.25.9", 32 | "@babel/core": "7.26.0", 33 | "@babel/preset-env": "7.26.0", 34 | "babel-plugin-add-module-exports": "1.0.4", 35 | "dtslint": "4.2.1", 36 | "esbuild": "0.25.0", 37 | "eslint": "8.55.0", 38 | "eslint-config-standard": "17.1.0", 39 | "eslint-plugin-import": "2.31.0", 40 | "eslint-plugin-n": "16.3.1", 41 | "eslint-plugin-promise": "6.1.1", 42 | "eslint-plugin-require-path-exists": "1.1.9", 43 | "eslint-plugin-standard": "5.0.0", 44 | "expect.js": "0.3.1", 45 | "i18next": "24.0.0", 46 | "js-yaml": "4.1.1", 47 | "jsonc-parser": "3.3.1", 48 | "json5": "2.2.3", 49 | "mocha": "10.8.2", 50 | "tslint": "5.20.1", 51 | "tsd": "0.31.2", 52 | "typescript": "5.6.3", 53 | "uglify-js": "3.19.3" 54 | }, 55 | "description": "i18next-fs-backend is a backend layer for i18next using in Node.js and for Deno to load translations from the filesystem.", 56 | "keywords": [ 57 | "i18next", 58 | "i18next-backend", 59 | "i18next-fs-backend" 60 | ], 61 | "homepage": "https://github.com/i18next/i18next-fs-backend", 62 | "repository": { 63 | "type": "git", 64 | "url": "git@github.com:i18next/i18next-fs-backend.git" 65 | }, 66 | "bugs": { 67 | "url": "https://github.com/i18next/i18next-fs-backend/issues" 68 | }, 69 | "license": "MIT", 70 | "config": { 71 | "fixcjs": "fs.writeFileSync('cjs/writeFile.js', fs.readFileSync('cjs/writeFile.js').toString().replace(`(await Promise.resolve().then(function () {\n return _interopRequireWildcard(require('node:fs'));\n})).default`, `require('node:fs')`));fs.writeFileSync('cjs/readFile.js', fs.readFileSync('cjs/readFile.js').toString().replace(`(await Promise.resolve().then(function () {\n return _interopRequireWildcard(require('node:fs'));\n})).default`, `require('node:fs')`))" 72 | }, 73 | "scripts": { 74 | "copy:jsonc": "esbuild node_modules/jsonc-parser/lib/esm/main.js --bundle --format=esm --platform=neutral --banner:js=\"/*\n$(sed 's/\\r$//' node_modules/jsonc-parser/LICENSE.md)\n*/\" --outfile=lib/formats/jsonc.js", 75 | "copy:json5": "cp node_modules/json5/dist/index.mjs lib/formats/json5.js", 76 | "copy:yaml": "cp node_modules/js-yaml/dist/js-yaml.mjs lib/formats/yaml.js", 77 | "copy": "rm -rf lib/formats && mkdir lib/formats && npm run copy:jsonc && npm run copy:json5 && npm run copy:yaml", 78 | "lint": "eslint .", 79 | "fixcjs": "node -e \"$npm_package_config_fixcjs\"", 80 | "compile:esm": "rm -rf esm && mkdir esm && BABEL_ENV=esm babel lib -d esm && cp index.d.ts esm/index.d.ts && cp index.d.mts esm/index.d.mts", 81 | "compile:cjs": "rm -rf cjs && mkdir cjs && BABEL_ENV=cjs babel lib -d cjs && cp index.d.ts cjs/index.d.ts && echo '{\"type\":\"commonjs\"}' > cjs/package.json && npm run fixcjs", 82 | "compile": "npm run copy && npm run compile:esm && npm run compile:cjs", 83 | "build": "npm run compile", 84 | "test": "npm run lint && npm run build && mocha test -R spec --exit --experimental-modules && npm run test:typescript", 85 | "test:typescript": "tslint --project tsconfig.json && tsd", 86 | "test:deno": "deno test test/deno/*.js --allow-read --allow-write --no-check", 87 | "test:bun": "bun test test/bun/*.js", 88 | "preversion": "npm run test && npm run build && git push", 89 | "postversion": "git push && git push --tags" 90 | }, 91 | "tsd": { 92 | "directory": "test/typescript" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [![Actions](https://github.com/i18next/i18next-fs-backend/workflows/node/badge.svg)](https://github.com/i18next/i18next-fs-backend/actions?query=workflow%3Anode) 4 | [![Actions deno](https://github.com/i18next/i18next-fs-backend/workflows/deno/badge.svg)](https://github.com/i18next/i18next-fs-backend/actions?query=workflow%3Adeno) 5 | [![npm version](https://img.shields.io/npm/v/i18next-fs-backend.svg?style=flat-square)](https://www.npmjs.com/package/i18next-fs-backend) 6 | 7 | This is an i18next backend to be used in Node.js and Deno. It will load resources from the file system. 8 | 9 | It's based on the deprecated [i18next-node-fs-backend](https://github.com/i18next/i18next-node-fs-backend) and can mostly be used as a drop-in replacement. 10 | 11 | It will load resources from filesystem. Right now it supports following filetypes: 12 | 13 | - .json 14 | - .json5 15 | - .jsonc 16 | - .yml/.yaml 17 | - .js (very limited, checks for `exports` or `export default`) 18 | 19 | # Getting started 20 | 21 | ```bash 22 | # npm package 23 | $ npm install i18next-fs-backend 24 | ``` 25 | 26 | Wiring up: 27 | 28 | ```js 29 | import i18next from 'i18next'; 30 | import Backend from 'i18next-fs-backend'; 31 | 32 | i18next.use(Backend).init(i18nextOptions); 33 | ``` 34 | 35 | for Deno: 36 | 37 | ```js 38 | import i18next from 'https://deno.land/x/i18next/index.js' 39 | import Backend from 'https://deno.land/x/i18next_fs_backend/index.js' 40 | 41 | i18next.use(Backend).init(i18nextOptions); 42 | ``` 43 | 44 | - As with all modules you can either pass the constructor function (class) to the i18next.use or a concrete instance. 45 | 46 | ## Backend Options 47 | 48 | ```js 49 | { 50 | // path where resources get loaded from, or a function 51 | // returning a path: 52 | // function(lngs, namespaces) { return customPath; } 53 | // the returned path will interpolate lng, ns if provided like giving a static path 54 | loadPath: '/locales/{{lng}}/{{ns}}.json', 55 | 56 | // path to post missing resources 57 | addPath: '/locales/{{lng}}/{{ns}}.missing.json', 58 | 59 | // if you use i18next-fs-backend as caching layer in combination with i18next-chained-backend, you can optionally set an expiration time 60 | // an example on how to use it as cache layer can be found here: https://github.com/i18next/i18next-fs-backend/blob/master/example/caching/app.js 61 | // expirationTime: 60 * 60 * 1000 62 | } 63 | ``` 64 | 65 | Options can be passed in: 66 | 67 | **preferred** - by setting options.backend in i18next.init: 68 | 69 | ```js 70 | import i18next from 'i18next'; 71 | import Backend from 'i18next-fs-backend'; 72 | 73 | i18next.use(Backend).init({ 74 | backend: options, 75 | }); 76 | ``` 77 | 78 | on construction: 79 | 80 | ```js 81 | import Backend from 'i18next-fs-backend'; 82 | const Backend = new Backend(null, options); 83 | ``` 84 | 85 | via calling init: 86 | 87 | ```js 88 | import Backend from 'i18next-fs-backend'; 89 | const Backend = new Backend(); 90 | Backend.init(null, options); 91 | ``` 92 | 93 | ## TypeScript 94 | 95 | To properly type the backend options, you can import the `FsBackendOptions` interface and use it as a generic type parameter to the i18next's `init` method, e.g.: 96 | 97 | ```ts 98 | import i18n from 'i18next' 99 | import FsBackend, { FsBackendOptions } from 'i18next-fs-backend' 100 | 101 | i18n 102 | .use(FsBackend) 103 | .init({ 104 | backend: { 105 | // fs backend options 106 | }, 107 | 108 | // other i18next options 109 | }) 110 | ``` 111 | 112 | # If set i18next initAsync option to false it will load the files synchronously 113 | 114 | ```js 115 | // i18n.js 116 | const { join } = require('path') 117 | const { readdirSync, lstatSync } = require('fs') 118 | const i18next = require('i18next') 119 | const Backend = require('i18next-fs-backend') 120 | i18next 121 | .use(Backend) 122 | .init({ 123 | // debug: true, 124 | initAsync: false, 125 | fallbackLng: 'en', 126 | lng: 'en', 127 | preload: readdirSync(join(__dirname, '../locales')).filter((fileName) => { 128 | const joinedPath = join(join(__dirname, '../locales'), fileName) 129 | const isDirectory = lstatSync(joinedPath).isDirectory() 130 | return isDirectory 131 | }), 132 | ns: 'backend-app', 133 | defaultNS: 'backend-app', 134 | backend: { 135 | loadPath: join(__dirname, '../locales/{{lng}}/{{ns}}.json') 136 | } 137 | }) 138 | ``` 139 | 140 | --- 141 | 142 |

Gold Sponsors

143 | 144 |

145 | 146 | 147 | 148 |

149 | -------------------------------------------------------------------------------- /lib/readFile.js: -------------------------------------------------------------------------------- 1 | import JSON5 from './formats/json5.js' 2 | import { parse as parseJSONC } from './formats/jsonc.js' 3 | import jsYaml from './formats/yaml.js' 4 | import extname from './extname.js' 5 | const isDeno = typeof Deno !== 'undefined' 6 | const isBun = typeof Bun !== 'undefined' 7 | const YAML = typeof jsYaml !== 'undefined' && jsYaml.load ? jsYaml : undefined 8 | const fs = (!isDeno/* && !isBun */) ? (await import('node:fs')).default : undefined 9 | // eslint-disable-next-line no-eval 10 | const evalAlias = eval 11 | 12 | const readFileInNodeSync = (filename) => { 13 | const data = fs.readFileSync(filename, 'utf8') 14 | let stat 15 | try { 16 | stat = fs.statSync(filename) 17 | } catch (e) {} 18 | return { data, stat } 19 | } 20 | 21 | const readFileInNode = (filename) => { 22 | return new Promise((resolve, reject) => { 23 | fs.readFile(filename, 'utf8', (err, data) => { 24 | if (err) return reject(err) 25 | fs.stat(filename, (err, stat) => { 26 | if (err) return resolve({ data }) 27 | return resolve({ data, stat }) 28 | }) 29 | }) 30 | }) 31 | } 32 | 33 | const readFileInDenoSync = (filename) => { 34 | const decoder = new TextDecoder('utf-8') 35 | // eslint-disable-next-line no-undef 36 | const d = Deno.readFileSync(filename) 37 | const data = decoder.decode(d) 38 | let stat 39 | try { 40 | // eslint-disable-next-line no-undef 41 | stat = Deno.statSync(filename) 42 | } catch (e) {} 43 | return { data, stat } 44 | } 45 | const readFileInDeno = (filename) => { 46 | return new Promise((resolve, reject) => { 47 | const decoder = new TextDecoder('utf-8') 48 | // eslint-disable-next-line no-undef 49 | Deno.readFile(filename).then((d) => { 50 | const data = decoder.decode(d) 51 | // eslint-disable-next-line no-undef 52 | Deno.stat(filename).then((stat) => resolve({ data, stat })).catch(() => resolve({ data })) 53 | }).catch(reject) 54 | }) 55 | } 56 | 57 | const readFileInBunSync = readFileInNodeSync 58 | const readFileInBun = readFileInNode 59 | // const readFileInBun = async (filename) => { 60 | // const f = Bun.file(filename) 61 | // const data = await f.text() 62 | // return { data } // Bun has no stat interface yet 63 | // } 64 | 65 | const replaceLast = (str, find, replace) => { 66 | const index = str.lastIndexOf(find) 67 | if (index > -1) { 68 | return str.substring(0, index) + replace + str.substring(index + find.length) 69 | } 70 | return str.toString() 71 | } 72 | 73 | const parseData = (extension, data, options) => { 74 | data = data.replace(/^\uFEFF/, '') 75 | let result = {} 76 | switch (extension) { 77 | case '.js': 78 | case '.ts': 79 | if (typeof module === 'undefined') { 80 | if (data.indexOf('exports') > -1) { // just to try... 81 | data = `(${replaceLast(data.substring(data.indexOf('=') + 1), '};', '')})` 82 | } else if (data.indexOf('export default ') > -1) { // just to try... 83 | data = `(${replaceLast(data.substring(data.indexOf('export default ') + 15), '};', '')})` 84 | } 85 | } 86 | result = evalAlias(data) 87 | break 88 | case '.json5': 89 | result = JSON5.parse(data) 90 | break 91 | case '.jsonc': 92 | result = parseJSONC(data) 93 | break 94 | case '.yml': 95 | case '.yaml': 96 | result = YAML.load(data) 97 | break 98 | default: 99 | result = options.parse(data) 100 | } 101 | return result 102 | } 103 | 104 | // const resolvePath = (filename) => { 105 | // return !path.isAbsolute(filename) && typeof process !== 'undefined' && process.cwd && !fs.existsSync(filename) ? path.join(process.cwd(), filename) : filename 106 | // } 107 | 108 | export function readFileSync (filename, options) { 109 | const ext = extname(filename) 110 | let data, stat 111 | if (isBun) { 112 | const ret = readFileInBunSync(filename) 113 | data = ret.data 114 | stat = ret.stat 115 | } else if (isDeno) { 116 | const ret = readFileInDenoSync(filename) 117 | data = ret.data 118 | stat = ret.stat 119 | } else { 120 | const ret = readFileInNodeSync(filename) 121 | data = ret.data 122 | stat = ret.stat 123 | } 124 | return { data: parseData(ext, data, options), stat } 125 | } 126 | 127 | export function readFile (filename, options = { parse: JSON.parse }) { 128 | const ext = extname(filename) 129 | // if (['.js', '.ts'].indexOf(ext) > -1) { 130 | // return import(resolvePath(filename)).then((imp) => { 131 | // return { data: (imp && imp.default) || imp } 132 | // }) 133 | // } 134 | const fn = isBun ? readFileInBun : isDeno ? readFileInDeno : readFileInNode 135 | return new Promise((resolve, reject) => { 136 | fn(filename).then(({ data, stat }) => { 137 | try { 138 | const ret = parseData(ext, data, options) 139 | resolve({ data: ret, stat }) 140 | } catch (err) { 141 | err.message = 'error parsing ' + filename + ': ' + err.message 142 | reject(err) 143 | } 144 | }).catch(reject) 145 | }) 146 | } 147 | -------------------------------------------------------------------------------- /lib/writeFile.js: -------------------------------------------------------------------------------- 1 | import JSON5 from './formats/json5.js' 2 | import jsYaml from './formats/yaml.js' 3 | import extname from './extname.js' 4 | const isDeno = typeof Deno !== 'undefined' 5 | const isBun = typeof Bun !== 'undefined' 6 | const YAML = typeof jsYaml !== 'undefined' && jsYaml.load ? jsYaml : undefined 7 | const fs = (!isDeno/* && !isBun */) ? (await import('node:fs')).default : undefined 8 | 9 | function dirname (path) { 10 | if (path.length === 0) return '.' 11 | let code = path.charCodeAt(0) 12 | const hasRoot = code === 47 13 | let end = -1 14 | let matchedSlash = true 15 | for (let i = path.length - 1; i >= 1; --i) { 16 | code = path.charCodeAt(i) 17 | if (code === 47) { 18 | if (!matchedSlash) { 19 | end = i 20 | break 21 | } 22 | } else { 23 | // We saw the first non-path separator 24 | matchedSlash = false 25 | } 26 | } 27 | 28 | if (end === -1) return hasRoot ? '/' : '.' 29 | if (hasRoot && end === 1) return '//' 30 | return path.slice(0, end) 31 | } 32 | 33 | const writeFileInNodeSync = (filename, payload) => { 34 | try { 35 | fs.mkdirSync(dirname(filename), { recursive: true }) 36 | } catch (err) {} 37 | return fs.writeFileSync(filename, payload, 'utf8') 38 | } 39 | 40 | const writeFileInNode = (filename, payload) => { 41 | return new Promise((resolve, reject) => { 42 | fs.mkdir(dirname(filename), { recursive: true }, () => { 43 | fs.writeFile(filename, payload, 'utf8', (err, data) => err ? reject(err) : resolve(data)) 44 | }) 45 | }) 46 | } 47 | 48 | const removeFileInNodeSync = (filename) => { 49 | return fs.unlinkSync(filename) 50 | } 51 | 52 | const removeFileInNode = (filename) => { 53 | return new Promise((resolve, reject) => fs.unlink(filename, (err) => err ? reject(err) : resolve())) 54 | } 55 | 56 | const writeFileInDenoSync = (filename, payload) => { 57 | const encoder = new TextEncoder() 58 | const data = encoder.encode(payload) 59 | try { 60 | // eslint-disable-next-line no-undef 61 | Deno.mkdirSync(dirname(filename), { recursive: true }) 62 | } catch (err) {} 63 | // eslint-disable-next-line no-undef 64 | Deno.writeFileSync(filename, data) 65 | } 66 | 67 | const writeFileInDeno = (filename, payload) => { 68 | const encoder = new TextEncoder() 69 | const data = encoder.encode(payload) 70 | return new Promise((resolve, reject) => { 71 | // eslint-disable-next-line no-undef 72 | Deno.mkdir(dirname(filename), { recursive: true }).then(() => { 73 | // eslint-disable-next-line no-undef 74 | Deno.writeFile(filename, data).then(resolve, reject) 75 | }).catch(() => { 76 | // eslint-disable-next-line no-undef 77 | Deno.writeFile(filename, data).then(resolve, reject) 78 | }) 79 | }) 80 | } 81 | 82 | const removeFileInDenoSync = (filename) => { 83 | // eslint-disable-next-line no-undef 84 | Deno.removeSync(filename) 85 | } 86 | 87 | const removeFileInDeno = (filename) => { 88 | // eslint-disable-next-line no-undef 89 | return Deno.remove(filename) 90 | } 91 | 92 | const writeFileInBunSync = writeFileInNodeSync // not yet a specific Bun interface 93 | 94 | const writeFileInBun = writeFileInNode 95 | // Bun.write generates some error warnings yet... 96 | // const writeFileInBun = (filename, payload) => Bun.write(filename, payload) 97 | 98 | const removeFileInBunSync = removeFileInNodeSync // not yet a specific Bun interface 99 | 100 | const removeFileInBun = removeFileInNode // not yet a specific Bun interface 101 | 102 | const stringifyData = (extension, data, options) => { 103 | let result = '' 104 | switch (extension) { 105 | case '.js': 106 | case '.ts': 107 | if (typeof module === 'undefined') { 108 | result = `export default ${options.stringify(data, null, options.ident)}` 109 | } else { 110 | result = `module.exports = ${options.stringify(data, null, options.ident)}` 111 | } 112 | break 113 | case '.json5': 114 | result = JSON5.stringify(data, null, options.ident) 115 | break 116 | case '.yml': 117 | case '.yaml': 118 | result = YAML.dump(data, { ident: options.indent }) 119 | break 120 | default: 121 | result = options.stringify(data, null, options.ident) 122 | } 123 | return result 124 | } 125 | 126 | export function writeFileSync (filename, payload, options) { 127 | const ext = extname(filename) 128 | let data 129 | try { 130 | data = stringifyData(ext, payload, options) 131 | } catch (err) { 132 | err.message = 'error stringifying ' + filename + ': ' + err.message 133 | throw err 134 | } 135 | if (isBun) { 136 | return writeFileInBunSync(filename, data) 137 | } else if (isDeno) { 138 | return writeFileInDenoSync(filename, data) 139 | } else { 140 | return writeFileInNodeSync(filename, data) 141 | } 142 | } 143 | 144 | export function writeFile (filename, payload, options = { stringify: JSON.stringify, ident: 2 }) { 145 | const ext = extname(filename) 146 | let data 147 | try { 148 | data = stringifyData(ext, payload, options) 149 | } catch (err) { 150 | err.message = 'error stringifying ' + filename + ': ' + err.message 151 | throw err 152 | } 153 | const fn = isBun ? writeFileInBun : isDeno ? writeFileInDeno : writeFileInNode 154 | return fn(filename, data) 155 | } 156 | 157 | export function removeFileSync (filename) { 158 | if (isBun) { 159 | return removeFileInBunSync(filename) 160 | } else if (isDeno) { 161 | return removeFileInDenoSync(filename) 162 | } else { 163 | return removeFileInNodeSync(filename) 164 | } 165 | } 166 | 167 | export function removeFile (filename) { 168 | const fn = isBun ? removeFileInBun : isDeno ? removeFileInDeno : removeFileInNode 169 | return fn(filename) 170 | } 171 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import { defaults, debounce, getPath, setPath, pushPath } from './utils.js' 2 | import { readFile, readFileSync } from './readFile.js' 3 | import { writeFile, removeFile } from './writeFile.js' 4 | 5 | const getDefaults = () => { 6 | return { 7 | loadPath: '/locales/{{lng}}/{{ns}}.json', 8 | addPath: '/locales/{{lng}}/{{ns}}.missing.json', 9 | ident: 2, 10 | parse: JSON.parse, 11 | stringify: JSON.stringify 12 | // expirationTime: 60 * 60 * 1000 13 | } 14 | } 15 | 16 | class Backend { 17 | constructor (services, options = {}, allOptions = {}) { 18 | this.services = services 19 | this.options = options 20 | this.allOptions = allOptions 21 | this.type = 'backend' 22 | this.init(services, options, allOptions) 23 | } 24 | 25 | init (services, options = {}, allOptions = {}) { 26 | this.services = services 27 | this.options = defaults(options, this.options || {}, getDefaults()) 28 | this.allOptions = allOptions 29 | this.queuedWrites = {} 30 | this.debouncedWrite = debounce(this.write, 250) 31 | } 32 | 33 | read (language, namespace, callback) { 34 | let loadPath = this.options.loadPath 35 | if (typeof this.options.loadPath === 'function') { 36 | loadPath = this.options.loadPath(language, namespace) 37 | } 38 | const filename = this.services.interpolator.interpolate(loadPath, { lng: language, ns: namespace }) 39 | if (this.allOptions.initAsync === false || this.allOptions.initImmediate === false) { 40 | try { 41 | const { data, stat } = readFileSync(filename, this.options) 42 | const timestamp = stat && stat.mtime && stat.mtime.getTime() 43 | if (this.options.expirationTime && timestamp && timestamp + this.options.expirationTime < Date.now()) { 44 | this.removeFile(language, namespace) 45 | return callback(new Error('File expired!'), false) // no retry 46 | } 47 | callback(null, data, timestamp) 48 | } catch (err) { 49 | callback(err, false) // no retry 50 | } 51 | return 52 | } 53 | readFile(filename, this.options) 54 | .then(({ data, stat }) => { 55 | const timestamp = stat && stat.mtime && stat.mtime.getTime() 56 | if (this.options.expirationTime && timestamp && timestamp + this.options.expirationTime < Date.now()) { 57 | this.removeFile(language, namespace) 58 | return callback(new Error('File expired!'), false) // no retry 59 | } 60 | callback(null, data, timestamp) 61 | }) 62 | .catch((err) => callback(err, false)) // no retry 63 | } 64 | 65 | create (languages, namespace, key, fallbackValue, callback) { 66 | if (typeof callback !== 'function') callback = () => {} 67 | if (typeof languages === 'string') languages = [languages] 68 | 69 | let todo = languages.length 70 | const done = () => { 71 | if (!--todo) callback() 72 | } 73 | 74 | languages.forEach((lng) => { 75 | // eslint-disable-next-line no-useless-call 76 | this.queue.call(this, lng, namespace, key, fallbackValue, done) 77 | }) 78 | } 79 | 80 | // this way i18next-fs-backend can be used as cache layer in combination with i18next-chained-backend 81 | save (language, namespace, data, callback) { 82 | if (!callback) callback = () => {} 83 | 84 | const keys = Object.keys(data) 85 | let todo = keys.length 86 | const done = () => { 87 | if (!--todo) callback() 88 | } 89 | 90 | keys.forEach((key) => { 91 | // eslint-disable-next-line no-useless-call 92 | this.queue.call(this, language, namespace, key, data[key], done) 93 | }) 94 | } 95 | 96 | removeFile (language, namespace) { 97 | let addPath = this.options.addPath 98 | if (typeof this.options.addPath === 'function') { 99 | addPath = this.options.addPath(language, namespace) 100 | } 101 | const filename = this.services.interpolator.interpolate(addPath, { lng: language, ns: namespace }) 102 | removeFile(filename, this.options) 103 | .then(() => {}) 104 | .catch(() => {}) 105 | } 106 | 107 | write () { 108 | for (const lng in this.queuedWrites) { 109 | const namespaces = this.queuedWrites[lng] 110 | if (lng !== 'locks') { 111 | for (const ns in namespaces) { 112 | this.writeFile(lng, ns) 113 | } 114 | } 115 | } 116 | } 117 | 118 | writeFile (lng, namespace) { 119 | const lock = getPath(this.queuedWrites, ['locks', lng, namespace]) 120 | if (lock) return 121 | 122 | let addPath = this.options.addPath 123 | if (typeof this.options.addPath === 'function') { 124 | addPath = this.options.addPath(lng, namespace) 125 | } 126 | 127 | const filename = this.services.interpolator.interpolate(addPath, { lng, ns: namespace }) 128 | 129 | const missings = getPath(this.queuedWrites, [lng, namespace]) 130 | setPath(this.queuedWrites, [lng, namespace], []) 131 | 132 | if (missings.length) { 133 | // lock 134 | setPath(this.queuedWrites, ['locks', lng, namespace], true) 135 | 136 | const proceed = ({ data }) => { 137 | missings.forEach((missing) => { 138 | const path = this.allOptions.keySeparator === false ? [missing.key] : (missing.key.split(this.allOptions.keySeparator || '.')) 139 | try { 140 | setPath(data, path, missing.fallbackValue) 141 | } catch (e) { 142 | if (path.length < 2 || !e.message || (e.message.indexOf('Cannot create property') < 0)) throw e 143 | setPath(data, [missing.key], missing.fallbackValue) 144 | } 145 | }) 146 | 147 | const proceedWrite = () => { 148 | // unlock 149 | setPath(this.queuedWrites, ['locks', lng, namespace], false) 150 | missings.forEach((missing) => { 151 | if (missing.callback) missing.callback() 152 | }) 153 | // rerun 154 | this.debouncedWrite() 155 | } 156 | writeFile(filename, data, this.options) 157 | .then(proceedWrite) 158 | .catch(proceedWrite) 159 | } 160 | readFile(filename, this.options).then(proceed).catch(() => proceed({ data: {} })) 161 | } 162 | } 163 | 164 | queue (lng, namespace, key, fallbackValue, callback) { 165 | pushPath(this.queuedWrites, [lng, namespace], { key, fallbackValue: fallbackValue || '', callback }) 166 | this.debouncedWrite() 167 | } 168 | } 169 | 170 | Backend.type = 'backend' 171 | 172 | export default Backend 173 | -------------------------------------------------------------------------------- /lib/formats/json5.js: -------------------------------------------------------------------------------- 1 | // This is a generated file. Do not edit. 2 | var Space_Separator = /[\u1680\u2000-\u200A\u202F\u205F\u3000]/; 3 | var ID_Start = /[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0980\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u09FC\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0AF9\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D\u0C58-\u0C5A\u0C60\u0C61\u0C80\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D54-\u0D56\u0D5F-\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u1884\u1887-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191E\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1C80-\u1C88\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005-\u3007\u3021-\u3029\u3031-\u3035\u3038-\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA69D\uA6A0-\uA6EF\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA8FD\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uA9E0-\uA9E4\uA9E6-\uA9EF\uA9FA-\uA9FE\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA7E-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDE80-\uDE9C\uDEA0-\uDED0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF75\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00\uDE10-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE4\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC03-\uDC37\uDC83-\uDCAF\uDCD0-\uDCE8\uDD03-\uDD26\uDD50-\uDD72\uDD76\uDD83-\uDDB2\uDDC1-\uDDC4\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE2B\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEDE\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3D\uDF50\uDF5D-\uDF61]|\uD805[\uDC00-\uDC34\uDC47-\uDC4A\uDC80-\uDCAF\uDCC4\uDCC5\uDCC7\uDD80-\uDDAE\uDDD8-\uDDDB\uDE00-\uDE2F\uDE44\uDE80-\uDEAA\uDF00-\uDF19]|\uD806[\uDCA0-\uDCDF\uDCFF\uDE00\uDE0B-\uDE32\uDE3A\uDE50\uDE5C-\uDE83\uDE86-\uDE89\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC2E\uDC40\uDC72-\uDC8F\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD30\uDD46]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDED0-\uDEED\uDF00-\uDF2F\uDF40-\uDF43\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50\uDF93-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB]|\uD83A[\uDC00-\uDCC4\uDD00-\uDD43]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]/; 4 | var ID_Continue = /[\xAA\xB5\xBA\xC0-\xD6\xD8-\xF6\xF8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0300-\u0374\u0376\u0377\u037A-\u037D\u037F\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u0483-\u0487\u048A-\u052F\u0531-\u0556\u0559\u0561-\u0587\u0591-\u05BD\u05BF\u05C1\u05C2\u05C4\u05C5\u05C7\u05D0-\u05EA\u05F0-\u05F2\u0610-\u061A\u0620-\u0669\u066E-\u06D3\u06D5-\u06DC\u06DF-\u06E8\u06EA-\u06FC\u06FF\u0710-\u074A\u074D-\u07B1\u07C0-\u07F5\u07FA\u0800-\u082D\u0840-\u085B\u0860-\u086A\u08A0-\u08B4\u08B6-\u08BD\u08D4-\u08E1\u08E3-\u0963\u0966-\u096F\u0971-\u0983\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BC-\u09C4\u09C7\u09C8\u09CB-\u09CE\u09D7\u09DC\u09DD\u09DF-\u09E3\u09E6-\u09F1\u09FC\u0A01-\u0A03\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A3C\u0A3E-\u0A42\u0A47\u0A48\u0A4B-\u0A4D\u0A51\u0A59-\u0A5C\u0A5E\u0A66-\u0A75\u0A81-\u0A83\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABC-\u0AC5\u0AC7-\u0AC9\u0ACB-\u0ACD\u0AD0\u0AE0-\u0AE3\u0AE6-\u0AEF\u0AF9-\u0AFF\u0B01-\u0B03\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3C-\u0B44\u0B47\u0B48\u0B4B-\u0B4D\u0B56\u0B57\u0B5C\u0B5D\u0B5F-\u0B63\u0B66-\u0B6F\u0B71\u0B82\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BBE-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCD\u0BD0\u0BD7\u0BE6-\u0BEF\u0C00-\u0C03\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C39\u0C3D-\u0C44\u0C46-\u0C48\u0C4A-\u0C4D\u0C55\u0C56\u0C58-\u0C5A\u0C60-\u0C63\u0C66-\u0C6F\u0C80-\u0C83\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBC-\u0CC4\u0CC6-\u0CC8\u0CCA-\u0CCD\u0CD5\u0CD6\u0CDE\u0CE0-\u0CE3\u0CE6-\u0CEF\u0CF1\u0CF2\u0D00-\u0D03\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D44\u0D46-\u0D48\u0D4A-\u0D4E\u0D54-\u0D57\u0D5F-\u0D63\u0D66-\u0D6F\u0D7A-\u0D7F\u0D82\u0D83\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0DCA\u0DCF-\u0DD4\u0DD6\u0DD8-\u0DDF\u0DE6-\u0DEF\u0DF2\u0DF3\u0E01-\u0E3A\u0E40-\u0E4E\u0E50-\u0E59\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB9\u0EBB-\u0EBD\u0EC0-\u0EC4\u0EC6\u0EC8-\u0ECD\u0ED0-\u0ED9\u0EDC-\u0EDF\u0F00\u0F18\u0F19\u0F20-\u0F29\u0F35\u0F37\u0F39\u0F3E-\u0F47\u0F49-\u0F6C\u0F71-\u0F84\u0F86-\u0F97\u0F99-\u0FBC\u0FC6\u1000-\u1049\u1050-\u109D\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u135D-\u135F\u1380-\u138F\u13A0-\u13F5\u13F8-\u13FD\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u16EE-\u16F8\u1700-\u170C\u170E-\u1714\u1720-\u1734\u1740-\u1753\u1760-\u176C\u176E-\u1770\u1772\u1773\u1780-\u17D3\u17D7\u17DC\u17DD\u17E0-\u17E9\u180B-\u180D\u1810-\u1819\u1820-\u1877\u1880-\u18AA\u18B0-\u18F5\u1900-\u191E\u1920-\u192B\u1930-\u193B\u1946-\u196D\u1970-\u1974\u1980-\u19AB\u19B0-\u19C9\u19D0-\u19D9\u1A00-\u1A1B\u1A20-\u1A5E\u1A60-\u1A7C\u1A7F-\u1A89\u1A90-\u1A99\u1AA7\u1AB0-\u1ABD\u1B00-\u1B4B\u1B50-\u1B59\u1B6B-\u1B73\u1B80-\u1BF3\u1C00-\u1C37\u1C40-\u1C49\u1C4D-\u1C7D\u1C80-\u1C88\u1CD0-\u1CD2\u1CD4-\u1CF9\u1D00-\u1DF9\u1DFB-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u203F\u2040\u2054\u2071\u207F\u2090-\u209C\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2160-\u2188\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D7F-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2DE0-\u2DFF\u2E2F\u3005-\u3007\u3021-\u302F\u3031-\u3035\u3038-\u303C\u3041-\u3096\u3099\u309A\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312E\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FEA\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA62B\uA640-\uA66F\uA674-\uA67D\uA67F-\uA6F1\uA717-\uA71F\uA722-\uA788\uA78B-\uA7AE\uA7B0-\uA7B7\uA7F7-\uA827\uA840-\uA873\uA880-\uA8C5\uA8D0-\uA8D9\uA8E0-\uA8F7\uA8FB\uA8FD\uA900-\uA92D\uA930-\uA953\uA960-\uA97C\uA980-\uA9C0\uA9CF-\uA9D9\uA9E0-\uA9FE\uAA00-\uAA36\uAA40-\uAA4D\uAA50-\uAA59\uAA60-\uAA76\uAA7A-\uAAC2\uAADB-\uAADD\uAAE0-\uAAEF\uAAF2-\uAAF6\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uAB30-\uAB5A\uAB5C-\uAB65\uAB70-\uABEA\uABEC\uABED\uABF0-\uABF9\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE00-\uFE0F\uFE20-\uFE2F\uFE33\uFE34\uFE4D-\uFE4F\uFE70-\uFE74\uFE76-\uFEFC\uFF10-\uFF19\uFF21-\uFF3A\uFF3F\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC]|\uD800[\uDC00-\uDC0B\uDC0D-\uDC26\uDC28-\uDC3A\uDC3C\uDC3D\uDC3F-\uDC4D\uDC50-\uDC5D\uDC80-\uDCFA\uDD40-\uDD74\uDDFD\uDE80-\uDE9C\uDEA0-\uDED0\uDEE0\uDF00-\uDF1F\uDF2D-\uDF4A\uDF50-\uDF7A\uDF80-\uDF9D\uDFA0-\uDFC3\uDFC8-\uDFCF\uDFD1-\uDFD5]|\uD801[\uDC00-\uDC9D\uDCA0-\uDCA9\uDCB0-\uDCD3\uDCD8-\uDCFB\uDD00-\uDD27\uDD30-\uDD63\uDE00-\uDF36\uDF40-\uDF55\uDF60-\uDF67]|\uD802[\uDC00-\uDC05\uDC08\uDC0A-\uDC35\uDC37\uDC38\uDC3C\uDC3F-\uDC55\uDC60-\uDC76\uDC80-\uDC9E\uDCE0-\uDCF2\uDCF4\uDCF5\uDD00-\uDD15\uDD20-\uDD39\uDD80-\uDDB7\uDDBE\uDDBF\uDE00-\uDE03\uDE05\uDE06\uDE0C-\uDE13\uDE15-\uDE17\uDE19-\uDE33\uDE38-\uDE3A\uDE3F\uDE60-\uDE7C\uDE80-\uDE9C\uDEC0-\uDEC7\uDEC9-\uDEE6\uDF00-\uDF35\uDF40-\uDF55\uDF60-\uDF72\uDF80-\uDF91]|\uD803[\uDC00-\uDC48\uDC80-\uDCB2\uDCC0-\uDCF2]|\uD804[\uDC00-\uDC46\uDC66-\uDC6F\uDC7F-\uDCBA\uDCD0-\uDCE8\uDCF0-\uDCF9\uDD00-\uDD34\uDD36-\uDD3F\uDD50-\uDD73\uDD76\uDD80-\uDDC4\uDDCA-\uDDCC\uDDD0-\uDDDA\uDDDC\uDE00-\uDE11\uDE13-\uDE37\uDE3E\uDE80-\uDE86\uDE88\uDE8A-\uDE8D\uDE8F-\uDE9D\uDE9F-\uDEA8\uDEB0-\uDEEA\uDEF0-\uDEF9\uDF00-\uDF03\uDF05-\uDF0C\uDF0F\uDF10\uDF13-\uDF28\uDF2A-\uDF30\uDF32\uDF33\uDF35-\uDF39\uDF3C-\uDF44\uDF47\uDF48\uDF4B-\uDF4D\uDF50\uDF57\uDF5D-\uDF63\uDF66-\uDF6C\uDF70-\uDF74]|\uD805[\uDC00-\uDC4A\uDC50-\uDC59\uDC80-\uDCC5\uDCC7\uDCD0-\uDCD9\uDD80-\uDDB5\uDDB8-\uDDC0\uDDD8-\uDDDD\uDE00-\uDE40\uDE44\uDE50-\uDE59\uDE80-\uDEB7\uDEC0-\uDEC9\uDF00-\uDF19\uDF1D-\uDF2B\uDF30-\uDF39]|\uD806[\uDCA0-\uDCE9\uDCFF\uDE00-\uDE3E\uDE47\uDE50-\uDE83\uDE86-\uDE99\uDEC0-\uDEF8]|\uD807[\uDC00-\uDC08\uDC0A-\uDC36\uDC38-\uDC40\uDC50-\uDC59\uDC72-\uDC8F\uDC92-\uDCA7\uDCA9-\uDCB6\uDD00-\uDD06\uDD08\uDD09\uDD0B-\uDD36\uDD3A\uDD3C\uDD3D\uDD3F-\uDD47\uDD50-\uDD59]|\uD808[\uDC00-\uDF99]|\uD809[\uDC00-\uDC6E\uDC80-\uDD43]|[\uD80C\uD81C-\uD820\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD80D[\uDC00-\uDC2E]|\uD811[\uDC00-\uDE46]|\uD81A[\uDC00-\uDE38\uDE40-\uDE5E\uDE60-\uDE69\uDED0-\uDEED\uDEF0-\uDEF4\uDF00-\uDF36\uDF40-\uDF43\uDF50-\uDF59\uDF63-\uDF77\uDF7D-\uDF8F]|\uD81B[\uDF00-\uDF44\uDF50-\uDF7E\uDF8F-\uDF9F\uDFE0\uDFE1]|\uD821[\uDC00-\uDFEC]|\uD822[\uDC00-\uDEF2]|\uD82C[\uDC00-\uDD1E\uDD70-\uDEFB]|\uD82F[\uDC00-\uDC6A\uDC70-\uDC7C\uDC80-\uDC88\uDC90-\uDC99\uDC9D\uDC9E]|\uD834[\uDD65-\uDD69\uDD6D-\uDD72\uDD7B-\uDD82\uDD85-\uDD8B\uDDAA-\uDDAD\uDE42-\uDE44]|\uD835[\uDC00-\uDC54\uDC56-\uDC9C\uDC9E\uDC9F\uDCA2\uDCA5\uDCA6\uDCA9-\uDCAC\uDCAE-\uDCB9\uDCBB\uDCBD-\uDCC3\uDCC5-\uDD05\uDD07-\uDD0A\uDD0D-\uDD14\uDD16-\uDD1C\uDD1E-\uDD39\uDD3B-\uDD3E\uDD40-\uDD44\uDD46\uDD4A-\uDD50\uDD52-\uDEA5\uDEA8-\uDEC0\uDEC2-\uDEDA\uDEDC-\uDEFA\uDEFC-\uDF14\uDF16-\uDF34\uDF36-\uDF4E\uDF50-\uDF6E\uDF70-\uDF88\uDF8A-\uDFA8\uDFAA-\uDFC2\uDFC4-\uDFCB\uDFCE-\uDFFF]|\uD836[\uDE00-\uDE36\uDE3B-\uDE6C\uDE75\uDE84\uDE9B-\uDE9F\uDEA1-\uDEAF]|\uD838[\uDC00-\uDC06\uDC08-\uDC18\uDC1B-\uDC21\uDC23\uDC24\uDC26-\uDC2A]|\uD83A[\uDC00-\uDCC4\uDCD0-\uDCD6\uDD00-\uDD4A\uDD50-\uDD59]|\uD83B[\uDE00-\uDE03\uDE05-\uDE1F\uDE21\uDE22\uDE24\uDE27\uDE29-\uDE32\uDE34-\uDE37\uDE39\uDE3B\uDE42\uDE47\uDE49\uDE4B\uDE4D-\uDE4F\uDE51\uDE52\uDE54\uDE57\uDE59\uDE5B\uDE5D\uDE5F\uDE61\uDE62\uDE64\uDE67-\uDE6A\uDE6C-\uDE72\uDE74-\uDE77\uDE79-\uDE7C\uDE7E\uDE80-\uDE89\uDE8B-\uDE9B\uDEA1-\uDEA3\uDEA5-\uDEA9\uDEAB-\uDEBB]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0]|\uD87E[\uDC00-\uDE1D]|\uDB40[\uDD00-\uDDEF]/; 5 | 6 | var unicode = { 7 | Space_Separator: Space_Separator, 8 | ID_Start: ID_Start, 9 | ID_Continue: ID_Continue 10 | }; 11 | 12 | var util = { 13 | isSpaceSeparator (c) { 14 | return typeof c === 'string' && unicode.Space_Separator.test(c) 15 | }, 16 | 17 | isIdStartChar (c) { 18 | return typeof c === 'string' && ( 19 | (c >= 'a' && c <= 'z') || 20 | (c >= 'A' && c <= 'Z') || 21 | (c === '$') || (c === '_') || 22 | unicode.ID_Start.test(c) 23 | ) 24 | }, 25 | 26 | isIdContinueChar (c) { 27 | return typeof c === 'string' && ( 28 | (c >= 'a' && c <= 'z') || 29 | (c >= 'A' && c <= 'Z') || 30 | (c >= '0' && c <= '9') || 31 | (c === '$') || (c === '_') || 32 | (c === '\u200C') || (c === '\u200D') || 33 | unicode.ID_Continue.test(c) 34 | ) 35 | }, 36 | 37 | isDigit (c) { 38 | return typeof c === 'string' && /[0-9]/.test(c) 39 | }, 40 | 41 | isHexDigit (c) { 42 | return typeof c === 'string' && /[0-9A-Fa-f]/.test(c) 43 | }, 44 | }; 45 | 46 | let source; 47 | let parseState; 48 | let stack; 49 | let pos; 50 | let line; 51 | let column; 52 | let token; 53 | let key; 54 | let root; 55 | 56 | var parse = function parse (text, reviver) { 57 | source = String(text); 58 | parseState = 'start'; 59 | stack = []; 60 | pos = 0; 61 | line = 1; 62 | column = 0; 63 | token = undefined; 64 | key = undefined; 65 | root = undefined; 66 | 67 | do { 68 | token = lex(); 69 | 70 | // This code is unreachable. 71 | // if (!parseStates[parseState]) { 72 | // throw invalidParseState() 73 | // } 74 | 75 | parseStates[parseState](); 76 | } while (token.type !== 'eof') 77 | 78 | if (typeof reviver === 'function') { 79 | return internalize({'': root}, '', reviver) 80 | } 81 | 82 | return root 83 | }; 84 | 85 | function internalize (holder, name, reviver) { 86 | const value = holder[name]; 87 | if (value != null && typeof value === 'object') { 88 | if (Array.isArray(value)) { 89 | for (let i = 0; i < value.length; i++) { 90 | const key = String(i); 91 | const replacement = internalize(value, key, reviver); 92 | if (replacement === undefined) { 93 | delete value[key]; 94 | } else { 95 | Object.defineProperty(value, key, { 96 | value: replacement, 97 | writable: true, 98 | enumerable: true, 99 | configurable: true, 100 | }); 101 | } 102 | } 103 | } else { 104 | for (const key in value) { 105 | const replacement = internalize(value, key, reviver); 106 | if (replacement === undefined) { 107 | delete value[key]; 108 | } else { 109 | Object.defineProperty(value, key, { 110 | value: replacement, 111 | writable: true, 112 | enumerable: true, 113 | configurable: true, 114 | }); 115 | } 116 | } 117 | } 118 | } 119 | 120 | return reviver.call(holder, name, value) 121 | } 122 | 123 | let lexState; 124 | let buffer; 125 | let doubleQuote; 126 | let sign; 127 | let c; 128 | 129 | function lex () { 130 | lexState = 'default'; 131 | buffer = ''; 132 | doubleQuote = false; 133 | sign = 1; 134 | 135 | for (;;) { 136 | c = peek(); 137 | 138 | // This code is unreachable. 139 | // if (!lexStates[lexState]) { 140 | // throw invalidLexState(lexState) 141 | // } 142 | 143 | const token = lexStates[lexState](); 144 | if (token) { 145 | return token 146 | } 147 | } 148 | } 149 | 150 | function peek () { 151 | if (source[pos]) { 152 | return String.fromCodePoint(source.codePointAt(pos)) 153 | } 154 | } 155 | 156 | function read () { 157 | const c = peek(); 158 | 159 | if (c === '\n') { 160 | line++; 161 | column = 0; 162 | } else if (c) { 163 | column += c.length; 164 | } else { 165 | column++; 166 | } 167 | 168 | if (c) { 169 | pos += c.length; 170 | } 171 | 172 | return c 173 | } 174 | 175 | const lexStates = { 176 | default () { 177 | switch (c) { 178 | case '\t': 179 | case '\v': 180 | case '\f': 181 | case ' ': 182 | case '\u00A0': 183 | case '\uFEFF': 184 | case '\n': 185 | case '\r': 186 | case '\u2028': 187 | case '\u2029': 188 | read(); 189 | return 190 | 191 | case '/': 192 | read(); 193 | lexState = 'comment'; 194 | return 195 | 196 | case undefined: 197 | read(); 198 | return newToken('eof') 199 | } 200 | 201 | if (util.isSpaceSeparator(c)) { 202 | read(); 203 | return 204 | } 205 | 206 | // This code is unreachable. 207 | // if (!lexStates[parseState]) { 208 | // throw invalidLexState(parseState) 209 | // } 210 | 211 | return lexStates[parseState]() 212 | }, 213 | 214 | comment () { 215 | switch (c) { 216 | case '*': 217 | read(); 218 | lexState = 'multiLineComment'; 219 | return 220 | 221 | case '/': 222 | read(); 223 | lexState = 'singleLineComment'; 224 | return 225 | } 226 | 227 | throw invalidChar(read()) 228 | }, 229 | 230 | multiLineComment () { 231 | switch (c) { 232 | case '*': 233 | read(); 234 | lexState = 'multiLineCommentAsterisk'; 235 | return 236 | 237 | case undefined: 238 | throw invalidChar(read()) 239 | } 240 | 241 | read(); 242 | }, 243 | 244 | multiLineCommentAsterisk () { 245 | switch (c) { 246 | case '*': 247 | read(); 248 | return 249 | 250 | case '/': 251 | read(); 252 | lexState = 'default'; 253 | return 254 | 255 | case undefined: 256 | throw invalidChar(read()) 257 | } 258 | 259 | read(); 260 | lexState = 'multiLineComment'; 261 | }, 262 | 263 | singleLineComment () { 264 | switch (c) { 265 | case '\n': 266 | case '\r': 267 | case '\u2028': 268 | case '\u2029': 269 | read(); 270 | lexState = 'default'; 271 | return 272 | 273 | case undefined: 274 | read(); 275 | return newToken('eof') 276 | } 277 | 278 | read(); 279 | }, 280 | 281 | value () { 282 | switch (c) { 283 | case '{': 284 | case '[': 285 | return newToken('punctuator', read()) 286 | 287 | case 'n': 288 | read(); 289 | literal('ull'); 290 | return newToken('null', null) 291 | 292 | case 't': 293 | read(); 294 | literal('rue'); 295 | return newToken('boolean', true) 296 | 297 | case 'f': 298 | read(); 299 | literal('alse'); 300 | return newToken('boolean', false) 301 | 302 | case '-': 303 | case '+': 304 | if (read() === '-') { 305 | sign = -1; 306 | } 307 | 308 | lexState = 'sign'; 309 | return 310 | 311 | case '.': 312 | buffer = read(); 313 | lexState = 'decimalPointLeading'; 314 | return 315 | 316 | case '0': 317 | buffer = read(); 318 | lexState = 'zero'; 319 | return 320 | 321 | case '1': 322 | case '2': 323 | case '3': 324 | case '4': 325 | case '5': 326 | case '6': 327 | case '7': 328 | case '8': 329 | case '9': 330 | buffer = read(); 331 | lexState = 'decimalInteger'; 332 | return 333 | 334 | case 'I': 335 | read(); 336 | literal('nfinity'); 337 | return newToken('numeric', Infinity) 338 | 339 | case 'N': 340 | read(); 341 | literal('aN'); 342 | return newToken('numeric', NaN) 343 | 344 | case '"': 345 | case "'": 346 | doubleQuote = (read() === '"'); 347 | buffer = ''; 348 | lexState = 'string'; 349 | return 350 | } 351 | 352 | throw invalidChar(read()) 353 | }, 354 | 355 | identifierNameStartEscape () { 356 | if (c !== 'u') { 357 | throw invalidChar(read()) 358 | } 359 | 360 | read(); 361 | const u = unicodeEscape(); 362 | switch (u) { 363 | case '$': 364 | case '_': 365 | break 366 | 367 | default: 368 | if (!util.isIdStartChar(u)) { 369 | throw invalidIdentifier() 370 | } 371 | 372 | break 373 | } 374 | 375 | buffer += u; 376 | lexState = 'identifierName'; 377 | }, 378 | 379 | identifierName () { 380 | switch (c) { 381 | case '$': 382 | case '_': 383 | case '\u200C': 384 | case '\u200D': 385 | buffer += read(); 386 | return 387 | 388 | case '\\': 389 | read(); 390 | lexState = 'identifierNameEscape'; 391 | return 392 | } 393 | 394 | if (util.isIdContinueChar(c)) { 395 | buffer += read(); 396 | return 397 | } 398 | 399 | return newToken('identifier', buffer) 400 | }, 401 | 402 | identifierNameEscape () { 403 | if (c !== 'u') { 404 | throw invalidChar(read()) 405 | } 406 | 407 | read(); 408 | const u = unicodeEscape(); 409 | switch (u) { 410 | case '$': 411 | case '_': 412 | case '\u200C': 413 | case '\u200D': 414 | break 415 | 416 | default: 417 | if (!util.isIdContinueChar(u)) { 418 | throw invalidIdentifier() 419 | } 420 | 421 | break 422 | } 423 | 424 | buffer += u; 425 | lexState = 'identifierName'; 426 | }, 427 | 428 | sign () { 429 | switch (c) { 430 | case '.': 431 | buffer = read(); 432 | lexState = 'decimalPointLeading'; 433 | return 434 | 435 | case '0': 436 | buffer = read(); 437 | lexState = 'zero'; 438 | return 439 | 440 | case '1': 441 | case '2': 442 | case '3': 443 | case '4': 444 | case '5': 445 | case '6': 446 | case '7': 447 | case '8': 448 | case '9': 449 | buffer = read(); 450 | lexState = 'decimalInteger'; 451 | return 452 | 453 | case 'I': 454 | read(); 455 | literal('nfinity'); 456 | return newToken('numeric', sign * Infinity) 457 | 458 | case 'N': 459 | read(); 460 | literal('aN'); 461 | return newToken('numeric', NaN) 462 | } 463 | 464 | throw invalidChar(read()) 465 | }, 466 | 467 | zero () { 468 | switch (c) { 469 | case '.': 470 | buffer += read(); 471 | lexState = 'decimalPoint'; 472 | return 473 | 474 | case 'e': 475 | case 'E': 476 | buffer += read(); 477 | lexState = 'decimalExponent'; 478 | return 479 | 480 | case 'x': 481 | case 'X': 482 | buffer += read(); 483 | lexState = 'hexadecimal'; 484 | return 485 | } 486 | 487 | return newToken('numeric', sign * 0) 488 | }, 489 | 490 | decimalInteger () { 491 | switch (c) { 492 | case '.': 493 | buffer += read(); 494 | lexState = 'decimalPoint'; 495 | return 496 | 497 | case 'e': 498 | case 'E': 499 | buffer += read(); 500 | lexState = 'decimalExponent'; 501 | return 502 | } 503 | 504 | if (util.isDigit(c)) { 505 | buffer += read(); 506 | return 507 | } 508 | 509 | return newToken('numeric', sign * Number(buffer)) 510 | }, 511 | 512 | decimalPointLeading () { 513 | if (util.isDigit(c)) { 514 | buffer += read(); 515 | lexState = 'decimalFraction'; 516 | return 517 | } 518 | 519 | throw invalidChar(read()) 520 | }, 521 | 522 | decimalPoint () { 523 | switch (c) { 524 | case 'e': 525 | case 'E': 526 | buffer += read(); 527 | lexState = 'decimalExponent'; 528 | return 529 | } 530 | 531 | if (util.isDigit(c)) { 532 | buffer += read(); 533 | lexState = 'decimalFraction'; 534 | return 535 | } 536 | 537 | return newToken('numeric', sign * Number(buffer)) 538 | }, 539 | 540 | decimalFraction () { 541 | switch (c) { 542 | case 'e': 543 | case 'E': 544 | buffer += read(); 545 | lexState = 'decimalExponent'; 546 | return 547 | } 548 | 549 | if (util.isDigit(c)) { 550 | buffer += read(); 551 | return 552 | } 553 | 554 | return newToken('numeric', sign * Number(buffer)) 555 | }, 556 | 557 | decimalExponent () { 558 | switch (c) { 559 | case '+': 560 | case '-': 561 | buffer += read(); 562 | lexState = 'decimalExponentSign'; 563 | return 564 | } 565 | 566 | if (util.isDigit(c)) { 567 | buffer += read(); 568 | lexState = 'decimalExponentInteger'; 569 | return 570 | } 571 | 572 | throw invalidChar(read()) 573 | }, 574 | 575 | decimalExponentSign () { 576 | if (util.isDigit(c)) { 577 | buffer += read(); 578 | lexState = 'decimalExponentInteger'; 579 | return 580 | } 581 | 582 | throw invalidChar(read()) 583 | }, 584 | 585 | decimalExponentInteger () { 586 | if (util.isDigit(c)) { 587 | buffer += read(); 588 | return 589 | } 590 | 591 | return newToken('numeric', sign * Number(buffer)) 592 | }, 593 | 594 | hexadecimal () { 595 | if (util.isHexDigit(c)) { 596 | buffer += read(); 597 | lexState = 'hexadecimalInteger'; 598 | return 599 | } 600 | 601 | throw invalidChar(read()) 602 | }, 603 | 604 | hexadecimalInteger () { 605 | if (util.isHexDigit(c)) { 606 | buffer += read(); 607 | return 608 | } 609 | 610 | return newToken('numeric', sign * Number(buffer)) 611 | }, 612 | 613 | string () { 614 | switch (c) { 615 | case '\\': 616 | read(); 617 | buffer += escape(); 618 | return 619 | 620 | case '"': 621 | if (doubleQuote) { 622 | read(); 623 | return newToken('string', buffer) 624 | } 625 | 626 | buffer += read(); 627 | return 628 | 629 | case "'": 630 | if (!doubleQuote) { 631 | read(); 632 | return newToken('string', buffer) 633 | } 634 | 635 | buffer += read(); 636 | return 637 | 638 | case '\n': 639 | case '\r': 640 | throw invalidChar(read()) 641 | 642 | case '\u2028': 643 | case '\u2029': 644 | separatorChar(c); 645 | break 646 | 647 | case undefined: 648 | throw invalidChar(read()) 649 | } 650 | 651 | buffer += read(); 652 | }, 653 | 654 | start () { 655 | switch (c) { 656 | case '{': 657 | case '[': 658 | return newToken('punctuator', read()) 659 | 660 | // This code is unreachable since the default lexState handles eof. 661 | // case undefined: 662 | // return newToken('eof') 663 | } 664 | 665 | lexState = 'value'; 666 | }, 667 | 668 | beforePropertyName () { 669 | switch (c) { 670 | case '$': 671 | case '_': 672 | buffer = read(); 673 | lexState = 'identifierName'; 674 | return 675 | 676 | case '\\': 677 | read(); 678 | lexState = 'identifierNameStartEscape'; 679 | return 680 | 681 | case '}': 682 | return newToken('punctuator', read()) 683 | 684 | case '"': 685 | case "'": 686 | doubleQuote = (read() === '"'); 687 | lexState = 'string'; 688 | return 689 | } 690 | 691 | if (util.isIdStartChar(c)) { 692 | buffer += read(); 693 | lexState = 'identifierName'; 694 | return 695 | } 696 | 697 | throw invalidChar(read()) 698 | }, 699 | 700 | afterPropertyName () { 701 | if (c === ':') { 702 | return newToken('punctuator', read()) 703 | } 704 | 705 | throw invalidChar(read()) 706 | }, 707 | 708 | beforePropertyValue () { 709 | lexState = 'value'; 710 | }, 711 | 712 | afterPropertyValue () { 713 | switch (c) { 714 | case ',': 715 | case '}': 716 | return newToken('punctuator', read()) 717 | } 718 | 719 | throw invalidChar(read()) 720 | }, 721 | 722 | beforeArrayValue () { 723 | if (c === ']') { 724 | return newToken('punctuator', read()) 725 | } 726 | 727 | lexState = 'value'; 728 | }, 729 | 730 | afterArrayValue () { 731 | switch (c) { 732 | case ',': 733 | case ']': 734 | return newToken('punctuator', read()) 735 | } 736 | 737 | throw invalidChar(read()) 738 | }, 739 | 740 | end () { 741 | // This code is unreachable since it's handled by the default lexState. 742 | // if (c === undefined) { 743 | // read() 744 | // return newToken('eof') 745 | // } 746 | 747 | throw invalidChar(read()) 748 | }, 749 | }; 750 | 751 | function newToken (type, value) { 752 | return { 753 | type, 754 | value, 755 | line, 756 | column, 757 | } 758 | } 759 | 760 | function literal (s) { 761 | for (const c of s) { 762 | const p = peek(); 763 | 764 | if (p !== c) { 765 | throw invalidChar(read()) 766 | } 767 | 768 | read(); 769 | } 770 | } 771 | 772 | function escape () { 773 | const c = peek(); 774 | switch (c) { 775 | case 'b': 776 | read(); 777 | return '\b' 778 | 779 | case 'f': 780 | read(); 781 | return '\f' 782 | 783 | case 'n': 784 | read(); 785 | return '\n' 786 | 787 | case 'r': 788 | read(); 789 | return '\r' 790 | 791 | case 't': 792 | read(); 793 | return '\t' 794 | 795 | case 'v': 796 | read(); 797 | return '\v' 798 | 799 | case '0': 800 | read(); 801 | if (util.isDigit(peek())) { 802 | throw invalidChar(read()) 803 | } 804 | 805 | return '\0' 806 | 807 | case 'x': 808 | read(); 809 | return hexEscape() 810 | 811 | case 'u': 812 | read(); 813 | return unicodeEscape() 814 | 815 | case '\n': 816 | case '\u2028': 817 | case '\u2029': 818 | read(); 819 | return '' 820 | 821 | case '\r': 822 | read(); 823 | if (peek() === '\n') { 824 | read(); 825 | } 826 | 827 | return '' 828 | 829 | case '1': 830 | case '2': 831 | case '3': 832 | case '4': 833 | case '5': 834 | case '6': 835 | case '7': 836 | case '8': 837 | case '9': 838 | throw invalidChar(read()) 839 | 840 | case undefined: 841 | throw invalidChar(read()) 842 | } 843 | 844 | return read() 845 | } 846 | 847 | function hexEscape () { 848 | let buffer = ''; 849 | let c = peek(); 850 | 851 | if (!util.isHexDigit(c)) { 852 | throw invalidChar(read()) 853 | } 854 | 855 | buffer += read(); 856 | 857 | c = peek(); 858 | if (!util.isHexDigit(c)) { 859 | throw invalidChar(read()) 860 | } 861 | 862 | buffer += read(); 863 | 864 | return String.fromCodePoint(parseInt(buffer, 16)) 865 | } 866 | 867 | function unicodeEscape () { 868 | let buffer = ''; 869 | let count = 4; 870 | 871 | while (count-- > 0) { 872 | const c = peek(); 873 | if (!util.isHexDigit(c)) { 874 | throw invalidChar(read()) 875 | } 876 | 877 | buffer += read(); 878 | } 879 | 880 | return String.fromCodePoint(parseInt(buffer, 16)) 881 | } 882 | 883 | const parseStates = { 884 | start () { 885 | if (token.type === 'eof') { 886 | throw invalidEOF() 887 | } 888 | 889 | push(); 890 | }, 891 | 892 | beforePropertyName () { 893 | switch (token.type) { 894 | case 'identifier': 895 | case 'string': 896 | key = token.value; 897 | parseState = 'afterPropertyName'; 898 | return 899 | 900 | case 'punctuator': 901 | // This code is unreachable since it's handled by the lexState. 902 | // if (token.value !== '}') { 903 | // throw invalidToken() 904 | // } 905 | 906 | pop(); 907 | return 908 | 909 | case 'eof': 910 | throw invalidEOF() 911 | } 912 | 913 | // This code is unreachable since it's handled by the lexState. 914 | // throw invalidToken() 915 | }, 916 | 917 | afterPropertyName () { 918 | // This code is unreachable since it's handled by the lexState. 919 | // if (token.type !== 'punctuator' || token.value !== ':') { 920 | // throw invalidToken() 921 | // } 922 | 923 | if (token.type === 'eof') { 924 | throw invalidEOF() 925 | } 926 | 927 | parseState = 'beforePropertyValue'; 928 | }, 929 | 930 | beforePropertyValue () { 931 | if (token.type === 'eof') { 932 | throw invalidEOF() 933 | } 934 | 935 | push(); 936 | }, 937 | 938 | beforeArrayValue () { 939 | if (token.type === 'eof') { 940 | throw invalidEOF() 941 | } 942 | 943 | if (token.type === 'punctuator' && token.value === ']') { 944 | pop(); 945 | return 946 | } 947 | 948 | push(); 949 | }, 950 | 951 | afterPropertyValue () { 952 | // This code is unreachable since it's handled by the lexState. 953 | // if (token.type !== 'punctuator') { 954 | // throw invalidToken() 955 | // } 956 | 957 | if (token.type === 'eof') { 958 | throw invalidEOF() 959 | } 960 | 961 | switch (token.value) { 962 | case ',': 963 | parseState = 'beforePropertyName'; 964 | return 965 | 966 | case '}': 967 | pop(); 968 | } 969 | 970 | // This code is unreachable since it's handled by the lexState. 971 | // throw invalidToken() 972 | }, 973 | 974 | afterArrayValue () { 975 | // This code is unreachable since it's handled by the lexState. 976 | // if (token.type !== 'punctuator') { 977 | // throw invalidToken() 978 | // } 979 | 980 | if (token.type === 'eof') { 981 | throw invalidEOF() 982 | } 983 | 984 | switch (token.value) { 985 | case ',': 986 | parseState = 'beforeArrayValue'; 987 | return 988 | 989 | case ']': 990 | pop(); 991 | } 992 | 993 | // This code is unreachable since it's handled by the lexState. 994 | // throw invalidToken() 995 | }, 996 | 997 | end () { 998 | // This code is unreachable since it's handled by the lexState. 999 | // if (token.type !== 'eof') { 1000 | // throw invalidToken() 1001 | // } 1002 | }, 1003 | }; 1004 | 1005 | function push () { 1006 | let value; 1007 | 1008 | switch (token.type) { 1009 | case 'punctuator': 1010 | switch (token.value) { 1011 | case '{': 1012 | value = {}; 1013 | break 1014 | 1015 | case '[': 1016 | value = []; 1017 | break 1018 | } 1019 | 1020 | break 1021 | 1022 | case 'null': 1023 | case 'boolean': 1024 | case 'numeric': 1025 | case 'string': 1026 | value = token.value; 1027 | break 1028 | 1029 | // This code is unreachable. 1030 | // default: 1031 | // throw invalidToken() 1032 | } 1033 | 1034 | if (root === undefined) { 1035 | root = value; 1036 | } else { 1037 | const parent = stack[stack.length - 1]; 1038 | if (Array.isArray(parent)) { 1039 | parent.push(value); 1040 | } else { 1041 | Object.defineProperty(parent, key, { 1042 | value, 1043 | writable: true, 1044 | enumerable: true, 1045 | configurable: true, 1046 | }); 1047 | } 1048 | } 1049 | 1050 | if (value !== null && typeof value === 'object') { 1051 | stack.push(value); 1052 | 1053 | if (Array.isArray(value)) { 1054 | parseState = 'beforeArrayValue'; 1055 | } else { 1056 | parseState = 'beforePropertyName'; 1057 | } 1058 | } else { 1059 | const current = stack[stack.length - 1]; 1060 | if (current == null) { 1061 | parseState = 'end'; 1062 | } else if (Array.isArray(current)) { 1063 | parseState = 'afterArrayValue'; 1064 | } else { 1065 | parseState = 'afterPropertyValue'; 1066 | } 1067 | } 1068 | } 1069 | 1070 | function pop () { 1071 | stack.pop(); 1072 | 1073 | const current = stack[stack.length - 1]; 1074 | if (current == null) { 1075 | parseState = 'end'; 1076 | } else if (Array.isArray(current)) { 1077 | parseState = 'afterArrayValue'; 1078 | } else { 1079 | parseState = 'afterPropertyValue'; 1080 | } 1081 | } 1082 | 1083 | // This code is unreachable. 1084 | // function invalidParseState () { 1085 | // return new Error(`JSON5: invalid parse state '${parseState}'`) 1086 | // } 1087 | 1088 | // This code is unreachable. 1089 | // function invalidLexState (state) { 1090 | // return new Error(`JSON5: invalid lex state '${state}'`) 1091 | // } 1092 | 1093 | function invalidChar (c) { 1094 | if (c === undefined) { 1095 | return syntaxError(`JSON5: invalid end of input at ${line}:${column}`) 1096 | } 1097 | 1098 | return syntaxError(`JSON5: invalid character '${formatChar(c)}' at ${line}:${column}`) 1099 | } 1100 | 1101 | function invalidEOF () { 1102 | return syntaxError(`JSON5: invalid end of input at ${line}:${column}`) 1103 | } 1104 | 1105 | // This code is unreachable. 1106 | // function invalidToken () { 1107 | // if (token.type === 'eof') { 1108 | // return syntaxError(`JSON5: invalid end of input at ${line}:${column}`) 1109 | // } 1110 | 1111 | // const c = String.fromCodePoint(token.value.codePointAt(0)) 1112 | // return syntaxError(`JSON5: invalid character '${formatChar(c)}' at ${line}:${column}`) 1113 | // } 1114 | 1115 | function invalidIdentifier () { 1116 | column -= 5; 1117 | return syntaxError(`JSON5: invalid identifier character at ${line}:${column}`) 1118 | } 1119 | 1120 | function separatorChar (c) { 1121 | console.warn(`JSON5: '${formatChar(c)}' in strings is not valid ECMAScript; consider escaping`); 1122 | } 1123 | 1124 | function formatChar (c) { 1125 | const replacements = { 1126 | "'": "\\'", 1127 | '"': '\\"', 1128 | '\\': '\\\\', 1129 | '\b': '\\b', 1130 | '\f': '\\f', 1131 | '\n': '\\n', 1132 | '\r': '\\r', 1133 | '\t': '\\t', 1134 | '\v': '\\v', 1135 | '\0': '\\0', 1136 | '\u2028': '\\u2028', 1137 | '\u2029': '\\u2029', 1138 | }; 1139 | 1140 | if (replacements[c]) { 1141 | return replacements[c] 1142 | } 1143 | 1144 | if (c < ' ') { 1145 | const hexString = c.charCodeAt(0).toString(16); 1146 | return '\\x' + ('00' + hexString).substring(hexString.length) 1147 | } 1148 | 1149 | return c 1150 | } 1151 | 1152 | function syntaxError (message) { 1153 | const err = new SyntaxError(message); 1154 | err.lineNumber = line; 1155 | err.columnNumber = column; 1156 | return err 1157 | } 1158 | 1159 | var stringify = function stringify (value, replacer, space) { 1160 | const stack = []; 1161 | let indent = ''; 1162 | let propertyList; 1163 | let replacerFunc; 1164 | let gap = ''; 1165 | let quote; 1166 | 1167 | if ( 1168 | replacer != null && 1169 | typeof replacer === 'object' && 1170 | !Array.isArray(replacer) 1171 | ) { 1172 | space = replacer.space; 1173 | quote = replacer.quote; 1174 | replacer = replacer.replacer; 1175 | } 1176 | 1177 | if (typeof replacer === 'function') { 1178 | replacerFunc = replacer; 1179 | } else if (Array.isArray(replacer)) { 1180 | propertyList = []; 1181 | for (const v of replacer) { 1182 | let item; 1183 | 1184 | if (typeof v === 'string') { 1185 | item = v; 1186 | } else if ( 1187 | typeof v === 'number' || 1188 | v instanceof String || 1189 | v instanceof Number 1190 | ) { 1191 | item = String(v); 1192 | } 1193 | 1194 | if (item !== undefined && propertyList.indexOf(item) < 0) { 1195 | propertyList.push(item); 1196 | } 1197 | } 1198 | } 1199 | 1200 | if (space instanceof Number) { 1201 | space = Number(space); 1202 | } else if (space instanceof String) { 1203 | space = String(space); 1204 | } 1205 | 1206 | if (typeof space === 'number') { 1207 | if (space > 0) { 1208 | space = Math.min(10, Math.floor(space)); 1209 | gap = ' '.substr(0, space); 1210 | } 1211 | } else if (typeof space === 'string') { 1212 | gap = space.substr(0, 10); 1213 | } 1214 | 1215 | return serializeProperty('', {'': value}) 1216 | 1217 | function serializeProperty (key, holder) { 1218 | let value = holder[key]; 1219 | if (value != null) { 1220 | if (typeof value.toJSON5 === 'function') { 1221 | value = value.toJSON5(key); 1222 | } else if (typeof value.toJSON === 'function') { 1223 | value = value.toJSON(key); 1224 | } 1225 | } 1226 | 1227 | if (replacerFunc) { 1228 | value = replacerFunc.call(holder, key, value); 1229 | } 1230 | 1231 | if (value instanceof Number) { 1232 | value = Number(value); 1233 | } else if (value instanceof String) { 1234 | value = String(value); 1235 | } else if (value instanceof Boolean) { 1236 | value = value.valueOf(); 1237 | } 1238 | 1239 | switch (value) { 1240 | case null: return 'null' 1241 | case true: return 'true' 1242 | case false: return 'false' 1243 | } 1244 | 1245 | if (typeof value === 'string') { 1246 | return quoteString(value, false) 1247 | } 1248 | 1249 | if (typeof value === 'number') { 1250 | return String(value) 1251 | } 1252 | 1253 | if (typeof value === 'object') { 1254 | return Array.isArray(value) ? serializeArray(value) : serializeObject(value) 1255 | } 1256 | 1257 | return undefined 1258 | } 1259 | 1260 | function quoteString (value) { 1261 | const quotes = { 1262 | "'": 0.1, 1263 | '"': 0.2, 1264 | }; 1265 | 1266 | const replacements = { 1267 | "'": "\\'", 1268 | '"': '\\"', 1269 | '\\': '\\\\', 1270 | '\b': '\\b', 1271 | '\f': '\\f', 1272 | '\n': '\\n', 1273 | '\r': '\\r', 1274 | '\t': '\\t', 1275 | '\v': '\\v', 1276 | '\0': '\\0', 1277 | '\u2028': '\\u2028', 1278 | '\u2029': '\\u2029', 1279 | }; 1280 | 1281 | let product = ''; 1282 | 1283 | for (let i = 0; i < value.length; i++) { 1284 | const c = value[i]; 1285 | switch (c) { 1286 | case "'": 1287 | case '"': 1288 | quotes[c]++; 1289 | product += c; 1290 | continue 1291 | 1292 | case '\0': 1293 | if (util.isDigit(value[i + 1])) { 1294 | product += '\\x00'; 1295 | continue 1296 | } 1297 | } 1298 | 1299 | if (replacements[c]) { 1300 | product += replacements[c]; 1301 | continue 1302 | } 1303 | 1304 | if (c < ' ') { 1305 | let hexString = c.charCodeAt(0).toString(16); 1306 | product += '\\x' + ('00' + hexString).substring(hexString.length); 1307 | continue 1308 | } 1309 | 1310 | product += c; 1311 | } 1312 | 1313 | const quoteChar = quote || Object.keys(quotes).reduce((a, b) => (quotes[a] < quotes[b]) ? a : b); 1314 | 1315 | product = product.replace(new RegExp(quoteChar, 'g'), replacements[quoteChar]); 1316 | 1317 | return quoteChar + product + quoteChar 1318 | } 1319 | 1320 | function serializeObject (value) { 1321 | if (stack.indexOf(value) >= 0) { 1322 | throw TypeError('Converting circular structure to JSON5') 1323 | } 1324 | 1325 | stack.push(value); 1326 | 1327 | let stepback = indent; 1328 | indent = indent + gap; 1329 | 1330 | let keys = propertyList || Object.keys(value); 1331 | let partial = []; 1332 | for (const key of keys) { 1333 | const propertyString = serializeProperty(key, value); 1334 | if (propertyString !== undefined) { 1335 | let member = serializeKey(key) + ':'; 1336 | if (gap !== '') { 1337 | member += ' '; 1338 | } 1339 | member += propertyString; 1340 | partial.push(member); 1341 | } 1342 | } 1343 | 1344 | let final; 1345 | if (partial.length === 0) { 1346 | final = '{}'; 1347 | } else { 1348 | let properties; 1349 | if (gap === '') { 1350 | properties = partial.join(','); 1351 | final = '{' + properties + '}'; 1352 | } else { 1353 | let separator = ',\n' + indent; 1354 | properties = partial.join(separator); 1355 | final = '{\n' + indent + properties + ',\n' + stepback + '}'; 1356 | } 1357 | } 1358 | 1359 | stack.pop(); 1360 | indent = stepback; 1361 | return final 1362 | } 1363 | 1364 | function serializeKey (key) { 1365 | if (key.length === 0) { 1366 | return quoteString(key, true) 1367 | } 1368 | 1369 | const firstChar = String.fromCodePoint(key.codePointAt(0)); 1370 | if (!util.isIdStartChar(firstChar)) { 1371 | return quoteString(key, true) 1372 | } 1373 | 1374 | for (let i = firstChar.length; i < key.length; i++) { 1375 | if (!util.isIdContinueChar(String.fromCodePoint(key.codePointAt(i)))) { 1376 | return quoteString(key, true) 1377 | } 1378 | } 1379 | 1380 | return key 1381 | } 1382 | 1383 | function serializeArray (value) { 1384 | if (stack.indexOf(value) >= 0) { 1385 | throw TypeError('Converting circular structure to JSON5') 1386 | } 1387 | 1388 | stack.push(value); 1389 | 1390 | let stepback = indent; 1391 | indent = indent + gap; 1392 | 1393 | let partial = []; 1394 | for (let i = 0; i < value.length; i++) { 1395 | const propertyString = serializeProperty(String(i), value); 1396 | partial.push((propertyString !== undefined) ? propertyString : 'null'); 1397 | } 1398 | 1399 | let final; 1400 | if (partial.length === 0) { 1401 | final = '[]'; 1402 | } else { 1403 | if (gap === '') { 1404 | let properties = partial.join(','); 1405 | final = '[' + properties + ']'; 1406 | } else { 1407 | let separator = ',\n' + indent; 1408 | let properties = partial.join(separator); 1409 | final = '[\n' + indent + properties + ',\n' + stepback + ']'; 1410 | } 1411 | } 1412 | 1413 | stack.pop(); 1414 | indent = stepback; 1415 | return final 1416 | } 1417 | }; 1418 | 1419 | const JSON5 = { 1420 | parse, 1421 | stringify, 1422 | }; 1423 | 1424 | var lib = JSON5; 1425 | 1426 | export default lib; 1427 | -------------------------------------------------------------------------------- /lib/formats/jsonc.js: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) Microsoft 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | // node_modules/jsonc-parser/lib/esm/impl/scanner.js 26 | function createScanner(text, ignoreTrivia = false) { 27 | const len = text.length; 28 | let pos = 0, value = "", tokenOffset = 0, token = 16, lineNumber = 0, lineStartOffset = 0, tokenLineStartOffset = 0, prevTokenLineStartOffset = 0, scanError = 0; 29 | function scanHexDigits(count, exact) { 30 | let digits = 0; 31 | let value2 = 0; 32 | while (digits < count || !exact) { 33 | let ch = text.charCodeAt(pos); 34 | if (ch >= 48 && ch <= 57) { 35 | value2 = value2 * 16 + ch - 48; 36 | } else if (ch >= 65 && ch <= 70) { 37 | value2 = value2 * 16 + ch - 65 + 10; 38 | } else if (ch >= 97 && ch <= 102) { 39 | value2 = value2 * 16 + ch - 97 + 10; 40 | } else { 41 | break; 42 | } 43 | pos++; 44 | digits++; 45 | } 46 | if (digits < count) { 47 | value2 = -1; 48 | } 49 | return value2; 50 | } 51 | function setPosition(newPosition) { 52 | pos = newPosition; 53 | value = ""; 54 | tokenOffset = 0; 55 | token = 16; 56 | scanError = 0; 57 | } 58 | function scanNumber() { 59 | let start = pos; 60 | if (text.charCodeAt(pos) === 48) { 61 | pos++; 62 | } else { 63 | pos++; 64 | while (pos < text.length && isDigit(text.charCodeAt(pos))) { 65 | pos++; 66 | } 67 | } 68 | if (pos < text.length && text.charCodeAt(pos) === 46) { 69 | pos++; 70 | if (pos < text.length && isDigit(text.charCodeAt(pos))) { 71 | pos++; 72 | while (pos < text.length && isDigit(text.charCodeAt(pos))) { 73 | pos++; 74 | } 75 | } else { 76 | scanError = 3; 77 | return text.substring(start, pos); 78 | } 79 | } 80 | let end = pos; 81 | if (pos < text.length && (text.charCodeAt(pos) === 69 || text.charCodeAt(pos) === 101)) { 82 | pos++; 83 | if (pos < text.length && text.charCodeAt(pos) === 43 || text.charCodeAt(pos) === 45) { 84 | pos++; 85 | } 86 | if (pos < text.length && isDigit(text.charCodeAt(pos))) { 87 | pos++; 88 | while (pos < text.length && isDigit(text.charCodeAt(pos))) { 89 | pos++; 90 | } 91 | end = pos; 92 | } else { 93 | scanError = 3; 94 | } 95 | } 96 | return text.substring(start, end); 97 | } 98 | function scanString() { 99 | let result = "", start = pos; 100 | while (true) { 101 | if (pos >= len) { 102 | result += text.substring(start, pos); 103 | scanError = 2; 104 | break; 105 | } 106 | const ch = text.charCodeAt(pos); 107 | if (ch === 34) { 108 | result += text.substring(start, pos); 109 | pos++; 110 | break; 111 | } 112 | if (ch === 92) { 113 | result += text.substring(start, pos); 114 | pos++; 115 | if (pos >= len) { 116 | scanError = 2; 117 | break; 118 | } 119 | const ch2 = text.charCodeAt(pos++); 120 | switch (ch2) { 121 | case 34: 122 | result += '"'; 123 | break; 124 | case 92: 125 | result += "\\"; 126 | break; 127 | case 47: 128 | result += "/"; 129 | break; 130 | case 98: 131 | result += "\b"; 132 | break; 133 | case 102: 134 | result += "\f"; 135 | break; 136 | case 110: 137 | result += "\n"; 138 | break; 139 | case 114: 140 | result += "\r"; 141 | break; 142 | case 116: 143 | result += " "; 144 | break; 145 | case 117: 146 | const ch3 = scanHexDigits(4, true); 147 | if (ch3 >= 0) { 148 | result += String.fromCharCode(ch3); 149 | } else { 150 | scanError = 4; 151 | } 152 | break; 153 | default: 154 | scanError = 5; 155 | } 156 | start = pos; 157 | continue; 158 | } 159 | if (ch >= 0 && ch <= 31) { 160 | if (isLineBreak(ch)) { 161 | result += text.substring(start, pos); 162 | scanError = 2; 163 | break; 164 | } else { 165 | scanError = 6; 166 | } 167 | } 168 | pos++; 169 | } 170 | return result; 171 | } 172 | function scanNext() { 173 | value = ""; 174 | scanError = 0; 175 | tokenOffset = pos; 176 | lineStartOffset = lineNumber; 177 | prevTokenLineStartOffset = tokenLineStartOffset; 178 | if (pos >= len) { 179 | tokenOffset = len; 180 | return token = 17; 181 | } 182 | let code = text.charCodeAt(pos); 183 | if (isWhiteSpace(code)) { 184 | do { 185 | pos++; 186 | value += String.fromCharCode(code); 187 | code = text.charCodeAt(pos); 188 | } while (isWhiteSpace(code)); 189 | return token = 15; 190 | } 191 | if (isLineBreak(code)) { 192 | pos++; 193 | value += String.fromCharCode(code); 194 | if (code === 13 && text.charCodeAt(pos) === 10) { 195 | pos++; 196 | value += "\n"; 197 | } 198 | lineNumber++; 199 | tokenLineStartOffset = pos; 200 | return token = 14; 201 | } 202 | switch (code) { 203 | // tokens: []{}:, 204 | case 123: 205 | pos++; 206 | return token = 1; 207 | case 125: 208 | pos++; 209 | return token = 2; 210 | case 91: 211 | pos++; 212 | return token = 3; 213 | case 93: 214 | pos++; 215 | return token = 4; 216 | case 58: 217 | pos++; 218 | return token = 6; 219 | case 44: 220 | pos++; 221 | return token = 5; 222 | // strings 223 | case 34: 224 | pos++; 225 | value = scanString(); 226 | return token = 10; 227 | // comments 228 | case 47: 229 | const start = pos - 1; 230 | if (text.charCodeAt(pos + 1) === 47) { 231 | pos += 2; 232 | while (pos < len) { 233 | if (isLineBreak(text.charCodeAt(pos))) { 234 | break; 235 | } 236 | pos++; 237 | } 238 | value = text.substring(start, pos); 239 | return token = 12; 240 | } 241 | if (text.charCodeAt(pos + 1) === 42) { 242 | pos += 2; 243 | const safeLength = len - 1; 244 | let commentClosed = false; 245 | while (pos < safeLength) { 246 | const ch = text.charCodeAt(pos); 247 | if (ch === 42 && text.charCodeAt(pos + 1) === 47) { 248 | pos += 2; 249 | commentClosed = true; 250 | break; 251 | } 252 | pos++; 253 | if (isLineBreak(ch)) { 254 | if (ch === 13 && text.charCodeAt(pos) === 10) { 255 | pos++; 256 | } 257 | lineNumber++; 258 | tokenLineStartOffset = pos; 259 | } 260 | } 261 | if (!commentClosed) { 262 | pos++; 263 | scanError = 1; 264 | } 265 | value = text.substring(start, pos); 266 | return token = 13; 267 | } 268 | value += String.fromCharCode(code); 269 | pos++; 270 | return token = 16; 271 | // numbers 272 | case 45: 273 | value += String.fromCharCode(code); 274 | pos++; 275 | if (pos === len || !isDigit(text.charCodeAt(pos))) { 276 | return token = 16; 277 | } 278 | // found a minus, followed by a number so 279 | // we fall through to proceed with scanning 280 | // numbers 281 | case 48: 282 | case 49: 283 | case 50: 284 | case 51: 285 | case 52: 286 | case 53: 287 | case 54: 288 | case 55: 289 | case 56: 290 | case 57: 291 | value += scanNumber(); 292 | return token = 11; 293 | // literals and unknown symbols 294 | default: 295 | while (pos < len && isUnknownContentCharacter(code)) { 296 | pos++; 297 | code = text.charCodeAt(pos); 298 | } 299 | if (tokenOffset !== pos) { 300 | value = text.substring(tokenOffset, pos); 301 | switch (value) { 302 | case "true": 303 | return token = 8; 304 | case "false": 305 | return token = 9; 306 | case "null": 307 | return token = 7; 308 | } 309 | return token = 16; 310 | } 311 | value += String.fromCharCode(code); 312 | pos++; 313 | return token = 16; 314 | } 315 | } 316 | function isUnknownContentCharacter(code) { 317 | if (isWhiteSpace(code) || isLineBreak(code)) { 318 | return false; 319 | } 320 | switch (code) { 321 | case 125: 322 | case 93: 323 | case 123: 324 | case 91: 325 | case 34: 326 | case 58: 327 | case 44: 328 | case 47: 329 | return false; 330 | } 331 | return true; 332 | } 333 | function scanNextNonTrivia() { 334 | let result; 335 | do { 336 | result = scanNext(); 337 | } while (result >= 12 && result <= 15); 338 | return result; 339 | } 340 | return { 341 | setPosition, 342 | getPosition: () => pos, 343 | scan: ignoreTrivia ? scanNextNonTrivia : scanNext, 344 | getToken: () => token, 345 | getTokenValue: () => value, 346 | getTokenOffset: () => tokenOffset, 347 | getTokenLength: () => pos - tokenOffset, 348 | getTokenStartLine: () => lineStartOffset, 349 | getTokenStartCharacter: () => tokenOffset - prevTokenLineStartOffset, 350 | getTokenError: () => scanError 351 | }; 352 | } 353 | function isWhiteSpace(ch) { 354 | return ch === 32 || ch === 9; 355 | } 356 | function isLineBreak(ch) { 357 | return ch === 10 || ch === 13; 358 | } 359 | function isDigit(ch) { 360 | return ch >= 48 && ch <= 57; 361 | } 362 | var CharacterCodes; 363 | (function(CharacterCodes2) { 364 | CharacterCodes2[CharacterCodes2["lineFeed"] = 10] = "lineFeed"; 365 | CharacterCodes2[CharacterCodes2["carriageReturn"] = 13] = "carriageReturn"; 366 | CharacterCodes2[CharacterCodes2["space"] = 32] = "space"; 367 | CharacterCodes2[CharacterCodes2["_0"] = 48] = "_0"; 368 | CharacterCodes2[CharacterCodes2["_1"] = 49] = "_1"; 369 | CharacterCodes2[CharacterCodes2["_2"] = 50] = "_2"; 370 | CharacterCodes2[CharacterCodes2["_3"] = 51] = "_3"; 371 | CharacterCodes2[CharacterCodes2["_4"] = 52] = "_4"; 372 | CharacterCodes2[CharacterCodes2["_5"] = 53] = "_5"; 373 | CharacterCodes2[CharacterCodes2["_6"] = 54] = "_6"; 374 | CharacterCodes2[CharacterCodes2["_7"] = 55] = "_7"; 375 | CharacterCodes2[CharacterCodes2["_8"] = 56] = "_8"; 376 | CharacterCodes2[CharacterCodes2["_9"] = 57] = "_9"; 377 | CharacterCodes2[CharacterCodes2["a"] = 97] = "a"; 378 | CharacterCodes2[CharacterCodes2["b"] = 98] = "b"; 379 | CharacterCodes2[CharacterCodes2["c"] = 99] = "c"; 380 | CharacterCodes2[CharacterCodes2["d"] = 100] = "d"; 381 | CharacterCodes2[CharacterCodes2["e"] = 101] = "e"; 382 | CharacterCodes2[CharacterCodes2["f"] = 102] = "f"; 383 | CharacterCodes2[CharacterCodes2["g"] = 103] = "g"; 384 | CharacterCodes2[CharacterCodes2["h"] = 104] = "h"; 385 | CharacterCodes2[CharacterCodes2["i"] = 105] = "i"; 386 | CharacterCodes2[CharacterCodes2["j"] = 106] = "j"; 387 | CharacterCodes2[CharacterCodes2["k"] = 107] = "k"; 388 | CharacterCodes2[CharacterCodes2["l"] = 108] = "l"; 389 | CharacterCodes2[CharacterCodes2["m"] = 109] = "m"; 390 | CharacterCodes2[CharacterCodes2["n"] = 110] = "n"; 391 | CharacterCodes2[CharacterCodes2["o"] = 111] = "o"; 392 | CharacterCodes2[CharacterCodes2["p"] = 112] = "p"; 393 | CharacterCodes2[CharacterCodes2["q"] = 113] = "q"; 394 | CharacterCodes2[CharacterCodes2["r"] = 114] = "r"; 395 | CharacterCodes2[CharacterCodes2["s"] = 115] = "s"; 396 | CharacterCodes2[CharacterCodes2["t"] = 116] = "t"; 397 | CharacterCodes2[CharacterCodes2["u"] = 117] = "u"; 398 | CharacterCodes2[CharacterCodes2["v"] = 118] = "v"; 399 | CharacterCodes2[CharacterCodes2["w"] = 119] = "w"; 400 | CharacterCodes2[CharacterCodes2["x"] = 120] = "x"; 401 | CharacterCodes2[CharacterCodes2["y"] = 121] = "y"; 402 | CharacterCodes2[CharacterCodes2["z"] = 122] = "z"; 403 | CharacterCodes2[CharacterCodes2["A"] = 65] = "A"; 404 | CharacterCodes2[CharacterCodes2["B"] = 66] = "B"; 405 | CharacterCodes2[CharacterCodes2["C"] = 67] = "C"; 406 | CharacterCodes2[CharacterCodes2["D"] = 68] = "D"; 407 | CharacterCodes2[CharacterCodes2["E"] = 69] = "E"; 408 | CharacterCodes2[CharacterCodes2["F"] = 70] = "F"; 409 | CharacterCodes2[CharacterCodes2["G"] = 71] = "G"; 410 | CharacterCodes2[CharacterCodes2["H"] = 72] = "H"; 411 | CharacterCodes2[CharacterCodes2["I"] = 73] = "I"; 412 | CharacterCodes2[CharacterCodes2["J"] = 74] = "J"; 413 | CharacterCodes2[CharacterCodes2["K"] = 75] = "K"; 414 | CharacterCodes2[CharacterCodes2["L"] = 76] = "L"; 415 | CharacterCodes2[CharacterCodes2["M"] = 77] = "M"; 416 | CharacterCodes2[CharacterCodes2["N"] = 78] = "N"; 417 | CharacterCodes2[CharacterCodes2["O"] = 79] = "O"; 418 | CharacterCodes2[CharacterCodes2["P"] = 80] = "P"; 419 | CharacterCodes2[CharacterCodes2["Q"] = 81] = "Q"; 420 | CharacterCodes2[CharacterCodes2["R"] = 82] = "R"; 421 | CharacterCodes2[CharacterCodes2["S"] = 83] = "S"; 422 | CharacterCodes2[CharacterCodes2["T"] = 84] = "T"; 423 | CharacterCodes2[CharacterCodes2["U"] = 85] = "U"; 424 | CharacterCodes2[CharacterCodes2["V"] = 86] = "V"; 425 | CharacterCodes2[CharacterCodes2["W"] = 87] = "W"; 426 | CharacterCodes2[CharacterCodes2["X"] = 88] = "X"; 427 | CharacterCodes2[CharacterCodes2["Y"] = 89] = "Y"; 428 | CharacterCodes2[CharacterCodes2["Z"] = 90] = "Z"; 429 | CharacterCodes2[CharacterCodes2["asterisk"] = 42] = "asterisk"; 430 | CharacterCodes2[CharacterCodes2["backslash"] = 92] = "backslash"; 431 | CharacterCodes2[CharacterCodes2["closeBrace"] = 125] = "closeBrace"; 432 | CharacterCodes2[CharacterCodes2["closeBracket"] = 93] = "closeBracket"; 433 | CharacterCodes2[CharacterCodes2["colon"] = 58] = "colon"; 434 | CharacterCodes2[CharacterCodes2["comma"] = 44] = "comma"; 435 | CharacterCodes2[CharacterCodes2["dot"] = 46] = "dot"; 436 | CharacterCodes2[CharacterCodes2["doubleQuote"] = 34] = "doubleQuote"; 437 | CharacterCodes2[CharacterCodes2["minus"] = 45] = "minus"; 438 | CharacterCodes2[CharacterCodes2["openBrace"] = 123] = "openBrace"; 439 | CharacterCodes2[CharacterCodes2["openBracket"] = 91] = "openBracket"; 440 | CharacterCodes2[CharacterCodes2["plus"] = 43] = "plus"; 441 | CharacterCodes2[CharacterCodes2["slash"] = 47] = "slash"; 442 | CharacterCodes2[CharacterCodes2["formFeed"] = 12] = "formFeed"; 443 | CharacterCodes2[CharacterCodes2["tab"] = 9] = "tab"; 444 | })(CharacterCodes || (CharacterCodes = {})); 445 | 446 | // node_modules/jsonc-parser/lib/esm/impl/string-intern.js 447 | var cachedSpaces = new Array(20).fill(0).map((_, index) => { 448 | return " ".repeat(index); 449 | }); 450 | var maxCachedValues = 200; 451 | var cachedBreakLinesWithSpaces = { 452 | " ": { 453 | "\n": new Array(maxCachedValues).fill(0).map((_, index) => { 454 | return "\n" + " ".repeat(index); 455 | }), 456 | "\r": new Array(maxCachedValues).fill(0).map((_, index) => { 457 | return "\r" + " ".repeat(index); 458 | }), 459 | "\r\n": new Array(maxCachedValues).fill(0).map((_, index) => { 460 | return "\r\n" + " ".repeat(index); 461 | }) 462 | }, 463 | " ": { 464 | "\n": new Array(maxCachedValues).fill(0).map((_, index) => { 465 | return "\n" + " ".repeat(index); 466 | }), 467 | "\r": new Array(maxCachedValues).fill(0).map((_, index) => { 468 | return "\r" + " ".repeat(index); 469 | }), 470 | "\r\n": new Array(maxCachedValues).fill(0).map((_, index) => { 471 | return "\r\n" + " ".repeat(index); 472 | }) 473 | } 474 | }; 475 | var supportedEols = ["\n", "\r", "\r\n"]; 476 | 477 | // node_modules/jsonc-parser/lib/esm/impl/format.js 478 | function format(documentText, range, options) { 479 | let initialIndentLevel; 480 | let formatText; 481 | let formatTextStart; 482 | let rangeStart; 483 | let rangeEnd; 484 | if (range) { 485 | rangeStart = range.offset; 486 | rangeEnd = rangeStart + range.length; 487 | formatTextStart = rangeStart; 488 | while (formatTextStart > 0 && !isEOL(documentText, formatTextStart - 1)) { 489 | formatTextStart--; 490 | } 491 | let endOffset = rangeEnd; 492 | while (endOffset < documentText.length && !isEOL(documentText, endOffset)) { 493 | endOffset++; 494 | } 495 | formatText = documentText.substring(formatTextStart, endOffset); 496 | initialIndentLevel = computeIndentLevel(formatText, options); 497 | } else { 498 | formatText = documentText; 499 | initialIndentLevel = 0; 500 | formatTextStart = 0; 501 | rangeStart = 0; 502 | rangeEnd = documentText.length; 503 | } 504 | const eol = getEOL(options, documentText); 505 | const eolFastPathSupported = supportedEols.includes(eol); 506 | let numberLineBreaks = 0; 507 | let indentLevel = 0; 508 | let indentValue; 509 | if (options.insertSpaces) { 510 | indentValue = cachedSpaces[options.tabSize || 4] ?? repeat(cachedSpaces[1], options.tabSize || 4); 511 | } else { 512 | indentValue = " "; 513 | } 514 | const indentType = indentValue === " " ? " " : " "; 515 | let scanner = createScanner(formatText, false); 516 | let hasError = false; 517 | function newLinesAndIndent() { 518 | if (numberLineBreaks > 1) { 519 | return repeat(eol, numberLineBreaks) + repeat(indentValue, initialIndentLevel + indentLevel); 520 | } 521 | const amountOfSpaces = indentValue.length * (initialIndentLevel + indentLevel); 522 | if (!eolFastPathSupported || amountOfSpaces > cachedBreakLinesWithSpaces[indentType][eol].length) { 523 | return eol + repeat(indentValue, initialIndentLevel + indentLevel); 524 | } 525 | if (amountOfSpaces <= 0) { 526 | return eol; 527 | } 528 | return cachedBreakLinesWithSpaces[indentType][eol][amountOfSpaces]; 529 | } 530 | function scanNext() { 531 | let token = scanner.scan(); 532 | numberLineBreaks = 0; 533 | while (token === 15 || token === 14) { 534 | if (token === 14 && options.keepLines) { 535 | numberLineBreaks += 1; 536 | } else if (token === 14) { 537 | numberLineBreaks = 1; 538 | } 539 | token = scanner.scan(); 540 | } 541 | hasError = token === 16 || scanner.getTokenError() !== 0; 542 | return token; 543 | } 544 | const editOperations = []; 545 | function addEdit(text, startOffset, endOffset) { 546 | if (!hasError && (!range || startOffset < rangeEnd && endOffset > rangeStart) && documentText.substring(startOffset, endOffset) !== text) { 547 | editOperations.push({ offset: startOffset, length: endOffset - startOffset, content: text }); 548 | } 549 | } 550 | let firstToken = scanNext(); 551 | if (options.keepLines && numberLineBreaks > 0) { 552 | addEdit(repeat(eol, numberLineBreaks), 0, 0); 553 | } 554 | if (firstToken !== 17) { 555 | let firstTokenStart = scanner.getTokenOffset() + formatTextStart; 556 | let initialIndent = indentValue.length * initialIndentLevel < 20 && options.insertSpaces ? cachedSpaces[indentValue.length * initialIndentLevel] : repeat(indentValue, initialIndentLevel); 557 | addEdit(initialIndent, formatTextStart, firstTokenStart); 558 | } 559 | while (firstToken !== 17) { 560 | let firstTokenEnd = scanner.getTokenOffset() + scanner.getTokenLength() + formatTextStart; 561 | let secondToken = scanNext(); 562 | let replaceContent = ""; 563 | let needsLineBreak = false; 564 | while (numberLineBreaks === 0 && (secondToken === 12 || secondToken === 13)) { 565 | let commentTokenStart = scanner.getTokenOffset() + formatTextStart; 566 | addEdit(cachedSpaces[1], firstTokenEnd, commentTokenStart); 567 | firstTokenEnd = scanner.getTokenOffset() + scanner.getTokenLength() + formatTextStart; 568 | needsLineBreak = secondToken === 12; 569 | replaceContent = needsLineBreak ? newLinesAndIndent() : ""; 570 | secondToken = scanNext(); 571 | } 572 | if (secondToken === 2) { 573 | if (firstToken !== 1) { 574 | indentLevel--; 575 | } 576 | ; 577 | if (options.keepLines && numberLineBreaks > 0 || !options.keepLines && firstToken !== 1) { 578 | replaceContent = newLinesAndIndent(); 579 | } else if (options.keepLines) { 580 | replaceContent = cachedSpaces[1]; 581 | } 582 | } else if (secondToken === 4) { 583 | if (firstToken !== 3) { 584 | indentLevel--; 585 | } 586 | ; 587 | if (options.keepLines && numberLineBreaks > 0 || !options.keepLines && firstToken !== 3) { 588 | replaceContent = newLinesAndIndent(); 589 | } else if (options.keepLines) { 590 | replaceContent = cachedSpaces[1]; 591 | } 592 | } else { 593 | switch (firstToken) { 594 | case 3: 595 | case 1: 596 | indentLevel++; 597 | if (options.keepLines && numberLineBreaks > 0 || !options.keepLines) { 598 | replaceContent = newLinesAndIndent(); 599 | } else { 600 | replaceContent = cachedSpaces[1]; 601 | } 602 | break; 603 | case 5: 604 | if (options.keepLines && numberLineBreaks > 0 || !options.keepLines) { 605 | replaceContent = newLinesAndIndent(); 606 | } else { 607 | replaceContent = cachedSpaces[1]; 608 | } 609 | break; 610 | case 12: 611 | replaceContent = newLinesAndIndent(); 612 | break; 613 | case 13: 614 | if (numberLineBreaks > 0) { 615 | replaceContent = newLinesAndIndent(); 616 | } else if (!needsLineBreak) { 617 | replaceContent = cachedSpaces[1]; 618 | } 619 | break; 620 | case 6: 621 | if (options.keepLines && numberLineBreaks > 0) { 622 | replaceContent = newLinesAndIndent(); 623 | } else if (!needsLineBreak) { 624 | replaceContent = cachedSpaces[1]; 625 | } 626 | break; 627 | case 10: 628 | if (options.keepLines && numberLineBreaks > 0) { 629 | replaceContent = newLinesAndIndent(); 630 | } else if (secondToken === 6 && !needsLineBreak) { 631 | replaceContent = ""; 632 | } 633 | break; 634 | case 7: 635 | case 8: 636 | case 9: 637 | case 11: 638 | case 2: 639 | case 4: 640 | if (options.keepLines && numberLineBreaks > 0) { 641 | replaceContent = newLinesAndIndent(); 642 | } else { 643 | if ((secondToken === 12 || secondToken === 13) && !needsLineBreak) { 644 | replaceContent = cachedSpaces[1]; 645 | } else if (secondToken !== 5 && secondToken !== 17) { 646 | hasError = true; 647 | } 648 | } 649 | break; 650 | case 16: 651 | hasError = true; 652 | break; 653 | } 654 | if (numberLineBreaks > 0 && (secondToken === 12 || secondToken === 13)) { 655 | replaceContent = newLinesAndIndent(); 656 | } 657 | } 658 | if (secondToken === 17) { 659 | if (options.keepLines && numberLineBreaks > 0) { 660 | replaceContent = newLinesAndIndent(); 661 | } else { 662 | replaceContent = options.insertFinalNewline ? eol : ""; 663 | } 664 | } 665 | const secondTokenStart = scanner.getTokenOffset() + formatTextStart; 666 | addEdit(replaceContent, firstTokenEnd, secondTokenStart); 667 | firstToken = secondToken; 668 | } 669 | return editOperations; 670 | } 671 | function repeat(s, count) { 672 | let result = ""; 673 | for (let i = 0; i < count; i++) { 674 | result += s; 675 | } 676 | return result; 677 | } 678 | function computeIndentLevel(content, options) { 679 | let i = 0; 680 | let nChars = 0; 681 | const tabSize = options.tabSize || 4; 682 | while (i < content.length) { 683 | let ch = content.charAt(i); 684 | if (ch === cachedSpaces[1]) { 685 | nChars++; 686 | } else if (ch === " ") { 687 | nChars += tabSize; 688 | } else { 689 | break; 690 | } 691 | i++; 692 | } 693 | return Math.floor(nChars / tabSize); 694 | } 695 | function getEOL(options, text) { 696 | for (let i = 0; i < text.length; i++) { 697 | const ch = text.charAt(i); 698 | if (ch === "\r") { 699 | if (i + 1 < text.length && text.charAt(i + 1) === "\n") { 700 | return "\r\n"; 701 | } 702 | return "\r"; 703 | } else if (ch === "\n") { 704 | return "\n"; 705 | } 706 | } 707 | return options && options.eol || "\n"; 708 | } 709 | function isEOL(text, offset) { 710 | return "\r\n".indexOf(text.charAt(offset)) !== -1; 711 | } 712 | 713 | // node_modules/jsonc-parser/lib/esm/impl/parser.js 714 | var ParseOptions; 715 | (function(ParseOptions2) { 716 | ParseOptions2.DEFAULT = { 717 | allowTrailingComma: false 718 | }; 719 | })(ParseOptions || (ParseOptions = {})); 720 | function getLocation(text, position) { 721 | const segments = []; 722 | const earlyReturnException = new Object(); 723 | let previousNode = void 0; 724 | const previousNodeInst = { 725 | value: {}, 726 | offset: 0, 727 | length: 0, 728 | type: "object", 729 | parent: void 0 730 | }; 731 | let isAtPropertyKey = false; 732 | function setPreviousNode(value, offset, length, type) { 733 | previousNodeInst.value = value; 734 | previousNodeInst.offset = offset; 735 | previousNodeInst.length = length; 736 | previousNodeInst.type = type; 737 | previousNodeInst.colonOffset = void 0; 738 | previousNode = previousNodeInst; 739 | } 740 | try { 741 | visit(text, { 742 | onObjectBegin: (offset, length) => { 743 | if (position <= offset) { 744 | throw earlyReturnException; 745 | } 746 | previousNode = void 0; 747 | isAtPropertyKey = position > offset; 748 | segments.push(""); 749 | }, 750 | onObjectProperty: (name, offset, length) => { 751 | if (position < offset) { 752 | throw earlyReturnException; 753 | } 754 | setPreviousNode(name, offset, length, "property"); 755 | segments[segments.length - 1] = name; 756 | if (position <= offset + length) { 757 | throw earlyReturnException; 758 | } 759 | }, 760 | onObjectEnd: (offset, length) => { 761 | if (position <= offset) { 762 | throw earlyReturnException; 763 | } 764 | previousNode = void 0; 765 | segments.pop(); 766 | }, 767 | onArrayBegin: (offset, length) => { 768 | if (position <= offset) { 769 | throw earlyReturnException; 770 | } 771 | previousNode = void 0; 772 | segments.push(0); 773 | }, 774 | onArrayEnd: (offset, length) => { 775 | if (position <= offset) { 776 | throw earlyReturnException; 777 | } 778 | previousNode = void 0; 779 | segments.pop(); 780 | }, 781 | onLiteralValue: (value, offset, length) => { 782 | if (position < offset) { 783 | throw earlyReturnException; 784 | } 785 | setPreviousNode(value, offset, length, getNodeType(value)); 786 | if (position <= offset + length) { 787 | throw earlyReturnException; 788 | } 789 | }, 790 | onSeparator: (sep, offset, length) => { 791 | if (position <= offset) { 792 | throw earlyReturnException; 793 | } 794 | if (sep === ":" && previousNode && previousNode.type === "property") { 795 | previousNode.colonOffset = offset; 796 | isAtPropertyKey = false; 797 | previousNode = void 0; 798 | } else if (sep === ",") { 799 | const last = segments[segments.length - 1]; 800 | if (typeof last === "number") { 801 | segments[segments.length - 1] = last + 1; 802 | } else { 803 | isAtPropertyKey = true; 804 | segments[segments.length - 1] = ""; 805 | } 806 | previousNode = void 0; 807 | } 808 | } 809 | }); 810 | } catch (e) { 811 | if (e !== earlyReturnException) { 812 | throw e; 813 | } 814 | } 815 | return { 816 | path: segments, 817 | previousNode, 818 | isAtPropertyKey, 819 | matches: (pattern) => { 820 | let k = 0; 821 | for (let i = 0; k < pattern.length && i < segments.length; i++) { 822 | if (pattern[k] === segments[i] || pattern[k] === "*") { 823 | k++; 824 | } else if (pattern[k] !== "**") { 825 | return false; 826 | } 827 | } 828 | return k === pattern.length; 829 | } 830 | }; 831 | } 832 | function parse(text, errors = [], options = ParseOptions.DEFAULT) { 833 | let currentProperty = null; 834 | let currentParent = []; 835 | const previousParents = []; 836 | function onValue(value) { 837 | if (Array.isArray(currentParent)) { 838 | currentParent.push(value); 839 | } else if (currentProperty !== null) { 840 | currentParent[currentProperty] = value; 841 | } 842 | } 843 | const visitor = { 844 | onObjectBegin: () => { 845 | const object = {}; 846 | onValue(object); 847 | previousParents.push(currentParent); 848 | currentParent = object; 849 | currentProperty = null; 850 | }, 851 | onObjectProperty: (name) => { 852 | currentProperty = name; 853 | }, 854 | onObjectEnd: () => { 855 | currentParent = previousParents.pop(); 856 | }, 857 | onArrayBegin: () => { 858 | const array = []; 859 | onValue(array); 860 | previousParents.push(currentParent); 861 | currentParent = array; 862 | currentProperty = null; 863 | }, 864 | onArrayEnd: () => { 865 | currentParent = previousParents.pop(); 866 | }, 867 | onLiteralValue: onValue, 868 | onError: (error, offset, length) => { 869 | errors.push({ error, offset, length }); 870 | } 871 | }; 872 | visit(text, visitor, options); 873 | return currentParent[0]; 874 | } 875 | function parseTree(text, errors = [], options = ParseOptions.DEFAULT) { 876 | let currentParent = { type: "array", offset: -1, length: -1, children: [], parent: void 0 }; 877 | function ensurePropertyComplete(endOffset) { 878 | if (currentParent.type === "property") { 879 | currentParent.length = endOffset - currentParent.offset; 880 | currentParent = currentParent.parent; 881 | } 882 | } 883 | function onValue(valueNode) { 884 | currentParent.children.push(valueNode); 885 | return valueNode; 886 | } 887 | const visitor = { 888 | onObjectBegin: (offset) => { 889 | currentParent = onValue({ type: "object", offset, length: -1, parent: currentParent, children: [] }); 890 | }, 891 | onObjectProperty: (name, offset, length) => { 892 | currentParent = onValue({ type: "property", offset, length: -1, parent: currentParent, children: [] }); 893 | currentParent.children.push({ type: "string", value: name, offset, length, parent: currentParent }); 894 | }, 895 | onObjectEnd: (offset, length) => { 896 | ensurePropertyComplete(offset + length); 897 | currentParent.length = offset + length - currentParent.offset; 898 | currentParent = currentParent.parent; 899 | ensurePropertyComplete(offset + length); 900 | }, 901 | onArrayBegin: (offset, length) => { 902 | currentParent = onValue({ type: "array", offset, length: -1, parent: currentParent, children: [] }); 903 | }, 904 | onArrayEnd: (offset, length) => { 905 | currentParent.length = offset + length - currentParent.offset; 906 | currentParent = currentParent.parent; 907 | ensurePropertyComplete(offset + length); 908 | }, 909 | onLiteralValue: (value, offset, length) => { 910 | onValue({ type: getNodeType(value), offset, length, parent: currentParent, value }); 911 | ensurePropertyComplete(offset + length); 912 | }, 913 | onSeparator: (sep, offset, length) => { 914 | if (currentParent.type === "property") { 915 | if (sep === ":") { 916 | currentParent.colonOffset = offset; 917 | } else if (sep === ",") { 918 | ensurePropertyComplete(offset); 919 | } 920 | } 921 | }, 922 | onError: (error, offset, length) => { 923 | errors.push({ error, offset, length }); 924 | } 925 | }; 926 | visit(text, visitor, options); 927 | const result = currentParent.children[0]; 928 | if (result) { 929 | delete result.parent; 930 | } 931 | return result; 932 | } 933 | function findNodeAtLocation(root, path) { 934 | if (!root) { 935 | return void 0; 936 | } 937 | let node = root; 938 | for (let segment of path) { 939 | if (typeof segment === "string") { 940 | if (node.type !== "object" || !Array.isArray(node.children)) { 941 | return void 0; 942 | } 943 | let found = false; 944 | for (const propertyNode of node.children) { 945 | if (Array.isArray(propertyNode.children) && propertyNode.children[0].value === segment && propertyNode.children.length === 2) { 946 | node = propertyNode.children[1]; 947 | found = true; 948 | break; 949 | } 950 | } 951 | if (!found) { 952 | return void 0; 953 | } 954 | } else { 955 | const index = segment; 956 | if (node.type !== "array" || index < 0 || !Array.isArray(node.children) || index >= node.children.length) { 957 | return void 0; 958 | } 959 | node = node.children[index]; 960 | } 961 | } 962 | return node; 963 | } 964 | function getNodePath(node) { 965 | if (!node.parent || !node.parent.children) { 966 | return []; 967 | } 968 | const path = getNodePath(node.parent); 969 | if (node.parent.type === "property") { 970 | const key = node.parent.children[0].value; 971 | path.push(key); 972 | } else if (node.parent.type === "array") { 973 | const index = node.parent.children.indexOf(node); 974 | if (index !== -1) { 975 | path.push(index); 976 | } 977 | } 978 | return path; 979 | } 980 | function getNodeValue(node) { 981 | switch (node.type) { 982 | case "array": 983 | return node.children.map(getNodeValue); 984 | case "object": 985 | const obj = /* @__PURE__ */ Object.create(null); 986 | for (let prop of node.children) { 987 | const valueNode = prop.children[1]; 988 | if (valueNode) { 989 | obj[prop.children[0].value] = getNodeValue(valueNode); 990 | } 991 | } 992 | return obj; 993 | case "null": 994 | case "string": 995 | case "number": 996 | case "boolean": 997 | return node.value; 998 | default: 999 | return void 0; 1000 | } 1001 | } 1002 | function contains(node, offset, includeRightBound = false) { 1003 | return offset >= node.offset && offset < node.offset + node.length || includeRightBound && offset === node.offset + node.length; 1004 | } 1005 | function findNodeAtOffset(node, offset, includeRightBound = false) { 1006 | if (contains(node, offset, includeRightBound)) { 1007 | const children = node.children; 1008 | if (Array.isArray(children)) { 1009 | for (let i = 0; i < children.length && children[i].offset <= offset; i++) { 1010 | const item = findNodeAtOffset(children[i], offset, includeRightBound); 1011 | if (item) { 1012 | return item; 1013 | } 1014 | } 1015 | } 1016 | return node; 1017 | } 1018 | return void 0; 1019 | } 1020 | function visit(text, visitor, options = ParseOptions.DEFAULT) { 1021 | const _scanner = createScanner(text, false); 1022 | const _jsonPath = []; 1023 | let suppressedCallbacks = 0; 1024 | function toNoArgVisit(visitFunction) { 1025 | return visitFunction ? () => suppressedCallbacks === 0 && visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true; 1026 | } 1027 | function toOneArgVisit(visitFunction) { 1028 | return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()) : () => true; 1029 | } 1030 | function toOneArgVisitWithPath(visitFunction) { 1031 | return visitFunction ? (arg) => suppressedCallbacks === 0 && visitFunction(arg, _scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()) : () => true; 1032 | } 1033 | function toBeginVisit(visitFunction) { 1034 | return visitFunction ? () => { 1035 | if (suppressedCallbacks > 0) { 1036 | suppressedCallbacks++; 1037 | } else { 1038 | let cbReturn = visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter(), () => _jsonPath.slice()); 1039 | if (cbReturn === false) { 1040 | suppressedCallbacks = 1; 1041 | } 1042 | } 1043 | } : () => true; 1044 | } 1045 | function toEndVisit(visitFunction) { 1046 | return visitFunction ? () => { 1047 | if (suppressedCallbacks > 0) { 1048 | suppressedCallbacks--; 1049 | } 1050 | if (suppressedCallbacks === 0) { 1051 | visitFunction(_scanner.getTokenOffset(), _scanner.getTokenLength(), _scanner.getTokenStartLine(), _scanner.getTokenStartCharacter()); 1052 | } 1053 | } : () => true; 1054 | } 1055 | const onObjectBegin = toBeginVisit(visitor.onObjectBegin), onObjectProperty = toOneArgVisitWithPath(visitor.onObjectProperty), onObjectEnd = toEndVisit(visitor.onObjectEnd), onArrayBegin = toBeginVisit(visitor.onArrayBegin), onArrayEnd = toEndVisit(visitor.onArrayEnd), onLiteralValue = toOneArgVisitWithPath(visitor.onLiteralValue), onSeparator = toOneArgVisit(visitor.onSeparator), onComment = toNoArgVisit(visitor.onComment), onError = toOneArgVisit(visitor.onError); 1056 | const disallowComments = options && options.disallowComments; 1057 | const allowTrailingComma = options && options.allowTrailingComma; 1058 | function scanNext() { 1059 | while (true) { 1060 | const token = _scanner.scan(); 1061 | switch (_scanner.getTokenError()) { 1062 | case 4: 1063 | handleError( 1064 | 14 1065 | /* ParseErrorCode.InvalidUnicode */ 1066 | ); 1067 | break; 1068 | case 5: 1069 | handleError( 1070 | 15 1071 | /* ParseErrorCode.InvalidEscapeCharacter */ 1072 | ); 1073 | break; 1074 | case 3: 1075 | handleError( 1076 | 13 1077 | /* ParseErrorCode.UnexpectedEndOfNumber */ 1078 | ); 1079 | break; 1080 | case 1: 1081 | if (!disallowComments) { 1082 | handleError( 1083 | 11 1084 | /* ParseErrorCode.UnexpectedEndOfComment */ 1085 | ); 1086 | } 1087 | break; 1088 | case 2: 1089 | handleError( 1090 | 12 1091 | /* ParseErrorCode.UnexpectedEndOfString */ 1092 | ); 1093 | break; 1094 | case 6: 1095 | handleError( 1096 | 16 1097 | /* ParseErrorCode.InvalidCharacter */ 1098 | ); 1099 | break; 1100 | } 1101 | switch (token) { 1102 | case 12: 1103 | case 13: 1104 | if (disallowComments) { 1105 | handleError( 1106 | 10 1107 | /* ParseErrorCode.InvalidCommentToken */ 1108 | ); 1109 | } else { 1110 | onComment(); 1111 | } 1112 | break; 1113 | case 16: 1114 | handleError( 1115 | 1 1116 | /* ParseErrorCode.InvalidSymbol */ 1117 | ); 1118 | break; 1119 | case 15: 1120 | case 14: 1121 | break; 1122 | default: 1123 | return token; 1124 | } 1125 | } 1126 | } 1127 | function handleError(error, skipUntilAfter = [], skipUntil = []) { 1128 | onError(error); 1129 | if (skipUntilAfter.length + skipUntil.length > 0) { 1130 | let token = _scanner.getToken(); 1131 | while (token !== 17) { 1132 | if (skipUntilAfter.indexOf(token) !== -1) { 1133 | scanNext(); 1134 | break; 1135 | } else if (skipUntil.indexOf(token) !== -1) { 1136 | break; 1137 | } 1138 | token = scanNext(); 1139 | } 1140 | } 1141 | } 1142 | function parseString(isValue) { 1143 | const value = _scanner.getTokenValue(); 1144 | if (isValue) { 1145 | onLiteralValue(value); 1146 | } else { 1147 | onObjectProperty(value); 1148 | _jsonPath.push(value); 1149 | } 1150 | scanNext(); 1151 | return true; 1152 | } 1153 | function parseLiteral() { 1154 | switch (_scanner.getToken()) { 1155 | case 11: 1156 | const tokenValue = _scanner.getTokenValue(); 1157 | let value = Number(tokenValue); 1158 | if (isNaN(value)) { 1159 | handleError( 1160 | 2 1161 | /* ParseErrorCode.InvalidNumberFormat */ 1162 | ); 1163 | value = 0; 1164 | } 1165 | onLiteralValue(value); 1166 | break; 1167 | case 7: 1168 | onLiteralValue(null); 1169 | break; 1170 | case 8: 1171 | onLiteralValue(true); 1172 | break; 1173 | case 9: 1174 | onLiteralValue(false); 1175 | break; 1176 | default: 1177 | return false; 1178 | } 1179 | scanNext(); 1180 | return true; 1181 | } 1182 | function parseProperty() { 1183 | if (_scanner.getToken() !== 10) { 1184 | handleError(3, [], [ 1185 | 2, 1186 | 5 1187 | /* SyntaxKind.CommaToken */ 1188 | ]); 1189 | return false; 1190 | } 1191 | parseString(false); 1192 | if (_scanner.getToken() === 6) { 1193 | onSeparator(":"); 1194 | scanNext(); 1195 | if (!parseValue()) { 1196 | handleError(4, [], [ 1197 | 2, 1198 | 5 1199 | /* SyntaxKind.CommaToken */ 1200 | ]); 1201 | } 1202 | } else { 1203 | handleError(5, [], [ 1204 | 2, 1205 | 5 1206 | /* SyntaxKind.CommaToken */ 1207 | ]); 1208 | } 1209 | _jsonPath.pop(); 1210 | return true; 1211 | } 1212 | function parseObject() { 1213 | onObjectBegin(); 1214 | scanNext(); 1215 | let needsComma = false; 1216 | while (_scanner.getToken() !== 2 && _scanner.getToken() !== 17) { 1217 | if (_scanner.getToken() === 5) { 1218 | if (!needsComma) { 1219 | handleError(4, [], []); 1220 | } 1221 | onSeparator(","); 1222 | scanNext(); 1223 | if (_scanner.getToken() === 2 && allowTrailingComma) { 1224 | break; 1225 | } 1226 | } else if (needsComma) { 1227 | handleError(6, [], []); 1228 | } 1229 | if (!parseProperty()) { 1230 | handleError(4, [], [ 1231 | 2, 1232 | 5 1233 | /* SyntaxKind.CommaToken */ 1234 | ]); 1235 | } 1236 | needsComma = true; 1237 | } 1238 | onObjectEnd(); 1239 | if (_scanner.getToken() !== 2) { 1240 | handleError(7, [ 1241 | 2 1242 | /* SyntaxKind.CloseBraceToken */ 1243 | ], []); 1244 | } else { 1245 | scanNext(); 1246 | } 1247 | return true; 1248 | } 1249 | function parseArray() { 1250 | onArrayBegin(); 1251 | scanNext(); 1252 | let isFirstElement = true; 1253 | let needsComma = false; 1254 | while (_scanner.getToken() !== 4 && _scanner.getToken() !== 17) { 1255 | if (_scanner.getToken() === 5) { 1256 | if (!needsComma) { 1257 | handleError(4, [], []); 1258 | } 1259 | onSeparator(","); 1260 | scanNext(); 1261 | if (_scanner.getToken() === 4 && allowTrailingComma) { 1262 | break; 1263 | } 1264 | } else if (needsComma) { 1265 | handleError(6, [], []); 1266 | } 1267 | if (isFirstElement) { 1268 | _jsonPath.push(0); 1269 | isFirstElement = false; 1270 | } else { 1271 | _jsonPath[_jsonPath.length - 1]++; 1272 | } 1273 | if (!parseValue()) { 1274 | handleError(4, [], [ 1275 | 4, 1276 | 5 1277 | /* SyntaxKind.CommaToken */ 1278 | ]); 1279 | } 1280 | needsComma = true; 1281 | } 1282 | onArrayEnd(); 1283 | if (!isFirstElement) { 1284 | _jsonPath.pop(); 1285 | } 1286 | if (_scanner.getToken() !== 4) { 1287 | handleError(8, [ 1288 | 4 1289 | /* SyntaxKind.CloseBracketToken */ 1290 | ], []); 1291 | } else { 1292 | scanNext(); 1293 | } 1294 | return true; 1295 | } 1296 | function parseValue() { 1297 | switch (_scanner.getToken()) { 1298 | case 3: 1299 | return parseArray(); 1300 | case 1: 1301 | return parseObject(); 1302 | case 10: 1303 | return parseString(true); 1304 | default: 1305 | return parseLiteral(); 1306 | } 1307 | } 1308 | scanNext(); 1309 | if (_scanner.getToken() === 17) { 1310 | if (options.allowEmptyContent) { 1311 | return true; 1312 | } 1313 | handleError(4, [], []); 1314 | return false; 1315 | } 1316 | if (!parseValue()) { 1317 | handleError(4, [], []); 1318 | return false; 1319 | } 1320 | if (_scanner.getToken() !== 17) { 1321 | handleError(9, [], []); 1322 | } 1323 | return true; 1324 | } 1325 | function stripComments(text, replaceCh) { 1326 | let _scanner = createScanner(text), parts = [], kind, offset = 0, pos; 1327 | do { 1328 | pos = _scanner.getPosition(); 1329 | kind = _scanner.scan(); 1330 | switch (kind) { 1331 | case 12: 1332 | case 13: 1333 | case 17: 1334 | if (offset !== pos) { 1335 | parts.push(text.substring(offset, pos)); 1336 | } 1337 | if (replaceCh !== void 0) { 1338 | parts.push(_scanner.getTokenValue().replace(/[^\r\n]/g, replaceCh)); 1339 | } 1340 | offset = _scanner.getPosition(); 1341 | break; 1342 | } 1343 | } while (kind !== 17); 1344 | return parts.join(""); 1345 | } 1346 | function getNodeType(value) { 1347 | switch (typeof value) { 1348 | case "boolean": 1349 | return "boolean"; 1350 | case "number": 1351 | return "number"; 1352 | case "string": 1353 | return "string"; 1354 | case "object": { 1355 | if (!value) { 1356 | return "null"; 1357 | } else if (Array.isArray(value)) { 1358 | return "array"; 1359 | } 1360 | return "object"; 1361 | } 1362 | default: 1363 | return "null"; 1364 | } 1365 | } 1366 | 1367 | // node_modules/jsonc-parser/lib/esm/impl/edit.js 1368 | function setProperty(text, originalPath, value, options) { 1369 | const path = originalPath.slice(); 1370 | const errors = []; 1371 | const root = parseTree(text, errors); 1372 | let parent = void 0; 1373 | let lastSegment = void 0; 1374 | while (path.length > 0) { 1375 | lastSegment = path.pop(); 1376 | parent = findNodeAtLocation(root, path); 1377 | if (parent === void 0 && value !== void 0) { 1378 | if (typeof lastSegment === "string") { 1379 | value = { [lastSegment]: value }; 1380 | } else { 1381 | value = [value]; 1382 | } 1383 | } else { 1384 | break; 1385 | } 1386 | } 1387 | if (!parent) { 1388 | if (value === void 0) { 1389 | throw new Error("Can not delete in empty document"); 1390 | } 1391 | return withFormatting(text, { offset: root ? root.offset : 0, length: root ? root.length : 0, content: JSON.stringify(value) }, options); 1392 | } else if (parent.type === "object" && typeof lastSegment === "string" && Array.isArray(parent.children)) { 1393 | const existing = findNodeAtLocation(parent, [lastSegment]); 1394 | if (existing !== void 0) { 1395 | if (value === void 0) { 1396 | if (!existing.parent) { 1397 | throw new Error("Malformed AST"); 1398 | } 1399 | const propertyIndex = parent.children.indexOf(existing.parent); 1400 | let removeBegin; 1401 | let removeEnd = existing.parent.offset + existing.parent.length; 1402 | if (propertyIndex > 0) { 1403 | let previous = parent.children[propertyIndex - 1]; 1404 | removeBegin = previous.offset + previous.length; 1405 | } else { 1406 | removeBegin = parent.offset + 1; 1407 | if (parent.children.length > 1) { 1408 | let next = parent.children[1]; 1409 | removeEnd = next.offset; 1410 | } 1411 | } 1412 | return withFormatting(text, { offset: removeBegin, length: removeEnd - removeBegin, content: "" }, options); 1413 | } else { 1414 | return withFormatting(text, { offset: existing.offset, length: existing.length, content: JSON.stringify(value) }, options); 1415 | } 1416 | } else { 1417 | if (value === void 0) { 1418 | return []; 1419 | } 1420 | const newProperty = `${JSON.stringify(lastSegment)}: ${JSON.stringify(value)}`; 1421 | const index = options.getInsertionIndex ? options.getInsertionIndex(parent.children.map((p) => p.children[0].value)) : parent.children.length; 1422 | let edit; 1423 | if (index > 0) { 1424 | let previous = parent.children[index - 1]; 1425 | edit = { offset: previous.offset + previous.length, length: 0, content: "," + newProperty }; 1426 | } else if (parent.children.length === 0) { 1427 | edit = { offset: parent.offset + 1, length: 0, content: newProperty }; 1428 | } else { 1429 | edit = { offset: parent.offset + 1, length: 0, content: newProperty + "," }; 1430 | } 1431 | return withFormatting(text, edit, options); 1432 | } 1433 | } else if (parent.type === "array" && typeof lastSegment === "number" && Array.isArray(parent.children)) { 1434 | const insertIndex = lastSegment; 1435 | if (insertIndex === -1) { 1436 | const newProperty = `${JSON.stringify(value)}`; 1437 | let edit; 1438 | if (parent.children.length === 0) { 1439 | edit = { offset: parent.offset + 1, length: 0, content: newProperty }; 1440 | } else { 1441 | const previous = parent.children[parent.children.length - 1]; 1442 | edit = { offset: previous.offset + previous.length, length: 0, content: "," + newProperty }; 1443 | } 1444 | return withFormatting(text, edit, options); 1445 | } else if (value === void 0 && parent.children.length >= 0) { 1446 | const removalIndex = lastSegment; 1447 | const toRemove = parent.children[removalIndex]; 1448 | let edit; 1449 | if (parent.children.length === 1) { 1450 | edit = { offset: parent.offset + 1, length: parent.length - 2, content: "" }; 1451 | } else if (parent.children.length - 1 === removalIndex) { 1452 | let previous = parent.children[removalIndex - 1]; 1453 | let offset = previous.offset + previous.length; 1454 | let parentEndOffset = parent.offset + parent.length; 1455 | edit = { offset, length: parentEndOffset - 2 - offset, content: "" }; 1456 | } else { 1457 | edit = { offset: toRemove.offset, length: parent.children[removalIndex + 1].offset - toRemove.offset, content: "" }; 1458 | } 1459 | return withFormatting(text, edit, options); 1460 | } else if (value !== void 0) { 1461 | let edit; 1462 | const newProperty = `${JSON.stringify(value)}`; 1463 | if (!options.isArrayInsertion && parent.children.length > lastSegment) { 1464 | const toModify = parent.children[lastSegment]; 1465 | edit = { offset: toModify.offset, length: toModify.length, content: newProperty }; 1466 | } else if (parent.children.length === 0 || lastSegment === 0) { 1467 | edit = { offset: parent.offset + 1, length: 0, content: parent.children.length === 0 ? newProperty : newProperty + "," }; 1468 | } else { 1469 | const index = lastSegment > parent.children.length ? parent.children.length : lastSegment; 1470 | const previous = parent.children[index - 1]; 1471 | edit = { offset: previous.offset + previous.length, length: 0, content: "," + newProperty }; 1472 | } 1473 | return withFormatting(text, edit, options); 1474 | } else { 1475 | throw new Error(`Can not ${value === void 0 ? "remove" : options.isArrayInsertion ? "insert" : "modify"} Array index ${insertIndex} as length is not sufficient`); 1476 | } 1477 | } else { 1478 | throw new Error(`Can not add ${typeof lastSegment !== "number" ? "index" : "property"} to parent of type ${parent.type}`); 1479 | } 1480 | } 1481 | function withFormatting(text, edit, options) { 1482 | if (!options.formattingOptions) { 1483 | return [edit]; 1484 | } 1485 | let newText = applyEdit(text, edit); 1486 | let begin = edit.offset; 1487 | let end = edit.offset + edit.content.length; 1488 | if (edit.length === 0 || edit.content.length === 0) { 1489 | while (begin > 0 && !isEOL(newText, begin - 1)) { 1490 | begin--; 1491 | } 1492 | while (end < newText.length && !isEOL(newText, end)) { 1493 | end++; 1494 | } 1495 | } 1496 | const edits = format(newText, { offset: begin, length: end - begin }, { ...options.formattingOptions, keepLines: false }); 1497 | for (let i = edits.length - 1; i >= 0; i--) { 1498 | const edit2 = edits[i]; 1499 | newText = applyEdit(newText, edit2); 1500 | begin = Math.min(begin, edit2.offset); 1501 | end = Math.max(end, edit2.offset + edit2.length); 1502 | end += edit2.content.length - edit2.length; 1503 | } 1504 | const editLength = text.length - (newText.length - end) - begin; 1505 | return [{ offset: begin, length: editLength, content: newText.substring(begin, end) }]; 1506 | } 1507 | function applyEdit(text, edit) { 1508 | return text.substring(0, edit.offset) + edit.content + text.substring(edit.offset + edit.length); 1509 | } 1510 | 1511 | // node_modules/jsonc-parser/lib/esm/main.js 1512 | var createScanner2 = createScanner; 1513 | var ScanError; 1514 | (function(ScanError2) { 1515 | ScanError2[ScanError2["None"] = 0] = "None"; 1516 | ScanError2[ScanError2["UnexpectedEndOfComment"] = 1] = "UnexpectedEndOfComment"; 1517 | ScanError2[ScanError2["UnexpectedEndOfString"] = 2] = "UnexpectedEndOfString"; 1518 | ScanError2[ScanError2["UnexpectedEndOfNumber"] = 3] = "UnexpectedEndOfNumber"; 1519 | ScanError2[ScanError2["InvalidUnicode"] = 4] = "InvalidUnicode"; 1520 | ScanError2[ScanError2["InvalidEscapeCharacter"] = 5] = "InvalidEscapeCharacter"; 1521 | ScanError2[ScanError2["InvalidCharacter"] = 6] = "InvalidCharacter"; 1522 | })(ScanError || (ScanError = {})); 1523 | var SyntaxKind; 1524 | (function(SyntaxKind2) { 1525 | SyntaxKind2[SyntaxKind2["OpenBraceToken"] = 1] = "OpenBraceToken"; 1526 | SyntaxKind2[SyntaxKind2["CloseBraceToken"] = 2] = "CloseBraceToken"; 1527 | SyntaxKind2[SyntaxKind2["OpenBracketToken"] = 3] = "OpenBracketToken"; 1528 | SyntaxKind2[SyntaxKind2["CloseBracketToken"] = 4] = "CloseBracketToken"; 1529 | SyntaxKind2[SyntaxKind2["CommaToken"] = 5] = "CommaToken"; 1530 | SyntaxKind2[SyntaxKind2["ColonToken"] = 6] = "ColonToken"; 1531 | SyntaxKind2[SyntaxKind2["NullKeyword"] = 7] = "NullKeyword"; 1532 | SyntaxKind2[SyntaxKind2["TrueKeyword"] = 8] = "TrueKeyword"; 1533 | SyntaxKind2[SyntaxKind2["FalseKeyword"] = 9] = "FalseKeyword"; 1534 | SyntaxKind2[SyntaxKind2["StringLiteral"] = 10] = "StringLiteral"; 1535 | SyntaxKind2[SyntaxKind2["NumericLiteral"] = 11] = "NumericLiteral"; 1536 | SyntaxKind2[SyntaxKind2["LineCommentTrivia"] = 12] = "LineCommentTrivia"; 1537 | SyntaxKind2[SyntaxKind2["BlockCommentTrivia"] = 13] = "BlockCommentTrivia"; 1538 | SyntaxKind2[SyntaxKind2["LineBreakTrivia"] = 14] = "LineBreakTrivia"; 1539 | SyntaxKind2[SyntaxKind2["Trivia"] = 15] = "Trivia"; 1540 | SyntaxKind2[SyntaxKind2["Unknown"] = 16] = "Unknown"; 1541 | SyntaxKind2[SyntaxKind2["EOF"] = 17] = "EOF"; 1542 | })(SyntaxKind || (SyntaxKind = {})); 1543 | var getLocation2 = getLocation; 1544 | var parse2 = parse; 1545 | var parseTree2 = parseTree; 1546 | var findNodeAtLocation2 = findNodeAtLocation; 1547 | var findNodeAtOffset2 = findNodeAtOffset; 1548 | var getNodePath2 = getNodePath; 1549 | var getNodeValue2 = getNodeValue; 1550 | var visit2 = visit; 1551 | var stripComments2 = stripComments; 1552 | var ParseErrorCode; 1553 | (function(ParseErrorCode2) { 1554 | ParseErrorCode2[ParseErrorCode2["InvalidSymbol"] = 1] = "InvalidSymbol"; 1555 | ParseErrorCode2[ParseErrorCode2["InvalidNumberFormat"] = 2] = "InvalidNumberFormat"; 1556 | ParseErrorCode2[ParseErrorCode2["PropertyNameExpected"] = 3] = "PropertyNameExpected"; 1557 | ParseErrorCode2[ParseErrorCode2["ValueExpected"] = 4] = "ValueExpected"; 1558 | ParseErrorCode2[ParseErrorCode2["ColonExpected"] = 5] = "ColonExpected"; 1559 | ParseErrorCode2[ParseErrorCode2["CommaExpected"] = 6] = "CommaExpected"; 1560 | ParseErrorCode2[ParseErrorCode2["CloseBraceExpected"] = 7] = "CloseBraceExpected"; 1561 | ParseErrorCode2[ParseErrorCode2["CloseBracketExpected"] = 8] = "CloseBracketExpected"; 1562 | ParseErrorCode2[ParseErrorCode2["EndOfFileExpected"] = 9] = "EndOfFileExpected"; 1563 | ParseErrorCode2[ParseErrorCode2["InvalidCommentToken"] = 10] = "InvalidCommentToken"; 1564 | ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfComment"] = 11] = "UnexpectedEndOfComment"; 1565 | ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfString"] = 12] = "UnexpectedEndOfString"; 1566 | ParseErrorCode2[ParseErrorCode2["UnexpectedEndOfNumber"] = 13] = "UnexpectedEndOfNumber"; 1567 | ParseErrorCode2[ParseErrorCode2["InvalidUnicode"] = 14] = "InvalidUnicode"; 1568 | ParseErrorCode2[ParseErrorCode2["InvalidEscapeCharacter"] = 15] = "InvalidEscapeCharacter"; 1569 | ParseErrorCode2[ParseErrorCode2["InvalidCharacter"] = 16] = "InvalidCharacter"; 1570 | })(ParseErrorCode || (ParseErrorCode = {})); 1571 | function printParseErrorCode(code) { 1572 | switch (code) { 1573 | case 1: 1574 | return "InvalidSymbol"; 1575 | case 2: 1576 | return "InvalidNumberFormat"; 1577 | case 3: 1578 | return "PropertyNameExpected"; 1579 | case 4: 1580 | return "ValueExpected"; 1581 | case 5: 1582 | return "ColonExpected"; 1583 | case 6: 1584 | return "CommaExpected"; 1585 | case 7: 1586 | return "CloseBraceExpected"; 1587 | case 8: 1588 | return "CloseBracketExpected"; 1589 | case 9: 1590 | return "EndOfFileExpected"; 1591 | case 10: 1592 | return "InvalidCommentToken"; 1593 | case 11: 1594 | return "UnexpectedEndOfComment"; 1595 | case 12: 1596 | return "UnexpectedEndOfString"; 1597 | case 13: 1598 | return "UnexpectedEndOfNumber"; 1599 | case 14: 1600 | return "InvalidUnicode"; 1601 | case 15: 1602 | return "InvalidEscapeCharacter"; 1603 | case 16: 1604 | return "InvalidCharacter"; 1605 | } 1606 | return ""; 1607 | } 1608 | function format2(documentText, range, options) { 1609 | return format(documentText, range, options); 1610 | } 1611 | function modify(text, path, value, options) { 1612 | return setProperty(text, path, value, options); 1613 | } 1614 | function applyEdits(text, edits) { 1615 | let sortedEdits = edits.slice(0).sort((a, b) => { 1616 | const diff = a.offset - b.offset; 1617 | if (diff === 0) { 1618 | return a.length - b.length; 1619 | } 1620 | return diff; 1621 | }); 1622 | let lastModifiedOffset = text.length; 1623 | for (let i = sortedEdits.length - 1; i >= 0; i--) { 1624 | let e = sortedEdits[i]; 1625 | if (e.offset + e.length <= lastModifiedOffset) { 1626 | text = applyEdit(text, e); 1627 | } else { 1628 | throw new Error("Overlapping edit"); 1629 | } 1630 | lastModifiedOffset = e.offset; 1631 | } 1632 | return text; 1633 | } 1634 | export { 1635 | ParseErrorCode, 1636 | ScanError, 1637 | SyntaxKind, 1638 | applyEdits, 1639 | createScanner2 as createScanner, 1640 | findNodeAtLocation2 as findNodeAtLocation, 1641 | findNodeAtOffset2 as findNodeAtOffset, 1642 | format2 as format, 1643 | getLocation2 as getLocation, 1644 | getNodePath2 as getNodePath, 1645 | getNodeValue2 as getNodeValue, 1646 | modify, 1647 | parse2 as parse, 1648 | parseTree2 as parseTree, 1649 | printParseErrorCode, 1650 | stripComments2 as stripComments, 1651 | visit2 as visit 1652 | }; 1653 | --------------------------------------------------------------------------------