├── .nvmrc ├── .gitattributes ├── packages ├── parjs │ ├── src │ │ ├── internal │ │ │ ├── functions │ │ │ │ ├── helpers.ts │ │ │ │ └── index.ts │ │ │ ├── combinated.ts │ │ │ ├── combinators │ │ │ │ ├── mapConst.ts │ │ │ │ ├── flatten.ts │ │ │ │ ├── backtrack.ts │ │ │ │ ├── each.ts │ │ │ │ ├── not.ts │ │ │ │ ├── maybe.ts │ │ │ │ ├── stringify.ts │ │ │ │ ├── later.ts │ │ │ │ ├── index.ts │ │ │ │ ├── between.ts │ │ │ │ ├── must.ts │ │ │ │ ├── map.ts │ │ │ │ ├── then-pick.ts │ │ │ │ ├── exactly.ts │ │ │ │ ├── must-capture.ts │ │ │ │ ├── many.ts │ │ │ │ ├── many1.ts │ │ │ │ ├── replace-state.ts │ │ │ │ ├── reason.ts │ │ │ │ ├── recover.ts │ │ │ │ ├── or.ts │ │ │ │ ├── then.ts │ │ │ │ ├── many-sep-by.ts │ │ │ │ └── many-till.ts │ │ │ ├── parsers │ │ │ │ ├── state.ts │ │ │ │ ├── position.ts │ │ │ │ ├── result.ts │ │ │ │ ├── rest.ts │ │ │ │ ├── eof.ts │ │ │ │ ├── index.ts │ │ │ │ ├── string-len.ts │ │ │ │ ├── numeric-helpers.ts │ │ │ │ ├── char-where.ts │ │ │ │ ├── fail.ts │ │ │ │ ├── char-code-where.ts │ │ │ │ ├── case-string.ts │ │ │ │ ├── string-of.ts │ │ │ │ ├── newline.ts │ │ │ │ └── int.ts │ │ │ ├── issues.ts │ │ │ ├── util-types.ts │ │ │ ├── index.ts │ │ │ ├── wrap-implicit.ts │ │ │ ├── trace-visualizer.ts │ │ │ ├── result.ts │ │ │ └── state.ts │ │ ├── tsconfig.json │ │ ├── internal.ts │ │ ├── combinators.ts │ │ ├── errors.ts │ │ ├── index.ts │ │ └── utils.ts │ ├── .npmignore │ ├── tools │ │ └── typedoc.cjs │ ├── jest.config.mjs │ ├── examples │ │ ├── tsconfig.json │ │ ├── src │ │ │ ├── tsconfig.json │ │ │ ├── tuple.ts │ │ │ └── ini.ts │ │ ├── spec │ │ │ ├── tuple.spec.ts │ │ │ ├── tsconfig.json │ │ │ ├── json.spec.ts │ │ │ └── index.ts │ │ ├── package.json │ │ └── jest.config.mjs │ ├── spec │ │ ├── unit │ │ │ ├── combinators │ │ │ │ ├── qthen.spec.ts │ │ │ │ ├── thenq.spec.ts │ │ │ │ ├── thenPick.spec.ts │ │ │ │ ├── exactly.spec.ts │ │ │ │ ├── expects.spec.ts │ │ │ │ ├── many1.spec.ts │ │ │ │ ├── recover.spec.ts │ │ │ │ ├── not.spec.ts │ │ │ │ ├── between.spec.ts │ │ │ │ ├── maybe.spec.ts │ │ │ │ ├── manyBetween.spec.ts │ │ │ │ ├── must.spec.ts │ │ │ │ ├── special.spec.ts │ │ │ │ ├── manyTill.spec.ts │ │ │ │ ├── or.spec.ts │ │ │ │ ├── flatten.spec.ts │ │ │ │ ├── many.spec.ts │ │ │ │ ├── manySepBy.spec.ts │ │ │ │ ├── mappers.spec.ts │ │ │ │ └── then.spec.ts │ │ │ ├── standalone │ │ │ │ ├── state.spec.ts │ │ │ │ ├── result.spec.ts │ │ │ │ ├── position.spec.ts │ │ │ │ ├── fail.spec.ts │ │ │ │ ├── eof.spec.ts │ │ │ │ ├── later.spec.ts │ │ │ │ ├── unicode.spec.ts │ │ │ │ └── int.spec.ts │ │ │ ├── expects.spec.ts │ │ │ ├── debug.spec.ts │ │ │ ├── anyChar.spec.ts │ │ │ └── trace.spec.ts │ │ ├── tsconfig.json │ │ └── utilities │ │ │ └── index.ts │ ├── tsconfig.json │ ├── LICENSE.md │ ├── CHANGELOG.md │ └── package.json └── char-info │ ├── src │ ├── names │ │ ├── index.ts │ │ ├── categories.ts │ │ └── scripts.ts │ ├── tsconfig.json │ ├── indicator-type.ts │ ├── index.ts │ ├── indicators.ts │ ├── unicode.ts │ └── unicode-lookup.ts │ ├── examples │ ├── tsconfig.json │ └── package.json │ ├── tsconfig.json │ ├── spec │ ├── tsconfig.json │ ├── indicator-test.ts │ └── ascii.spec.ts │ ├── LICENSE.md │ ├── package.json │ └── README.md ├── documentation ├── architecture-design-records │ ├── outdated │ │ └── .gitkeep │ ├── 002-switch-testing-framework-to-jest.md │ ├── 001-document-decisions.md │ └── 003-release-stable-version.md ├── contributing.md └── using-parjs.md ├── .yarnrc.yml ├── .prettierignore ├── .vscode ├── extensions.json ├── tasks.json └── settings.json ├── .editorconfig ├── tsconfig.json ├── tsconfig.base.json ├── .prettierrc.json ├── jest.root.mjs ├── .istanbul.yml ├── LICENSE.md ├── .markdownlint-cli2.jsonc ├── .github └── workflows │ ├── pr.yaml │ ├── parjs.push.yaml │ └── char-info.push.yaml ├── .gitignore ├── package.json └── parjs.code-workspace /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.13.0 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js linguist-generated=true -------------------------------------------------------------------------------- /packages/parjs/src/internal/functions/helpers.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /documentation/architecture-design-records/outdated/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/parjs/.npmignore: -------------------------------------------------------------------------------- 1 | **/*.tsbuildinfo 2 | **/tsconfig*.json -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | npmPublishRegistry: "https://registry.npmjs.org/" 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | **/node_modules/ 3 | **/dist/ 4 | **/dist-spec/ 5 | coverage/ 6 | .nyc_output/ 7 | .obsidian/ 8 | .git/ 9 | *.md -------------------------------------------------------------------------------- /packages/char-info/src/names/index.ts: -------------------------------------------------------------------------------- 1 | /** @external */ export { UnicodeBlock } from "./blocks"; 2 | export { UnicodeCategory } from "./categories"; 3 | export { UnicodeScript } from "./scripts"; 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "Orta.vscode-jest", 6 | "aaron-bond.better-comments" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/char-info/examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "baseUrl": "." 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.*] 4 | charset = utf-8 5 | indent_style = space 6 | end_of_line = lf 7 | 8 | [**/*.{yml, json}] 9 | indent_size = 2 10 | 11 | [src/**/*.ts] 12 | indent_size = 4 13 | -------------------------------------------------------------------------------- /packages/parjs/tools/typedoc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import("typedoc").TypeDocOptions} */ 2 | module.exports = { 3 | entryPoints: ["./src/lib/index.ts", "./src/lib/combinators.ts", "./src/lib/internal.ts"], 4 | out: "docs" 5 | }; 6 | -------------------------------------------------------------------------------- /packages/char-info/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist", 5 | "rootDir": ".", 6 | "noEmit": false 7 | }, 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/parjs/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "noEmit": false, 6 | "outDir": "../dist" 7 | }, 8 | "include": ["**/*.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/parjs/src/internal.ts: -------------------------------------------------------------------------------- 1 | export { ParjserBase, composeCombinator, defineCombinator } from "./internal/index"; 2 | export type { ParjsFailure, ParsingState } from "./internal/index"; 3 | export { isParjsFailure, isParjsResult, isParjsSuccess } from "./internal/result"; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./packages/char-info/tsconfig.json" 5 | }, 6 | { 7 | "path": "./packages/parjs/tsconfig.json" 8 | } 9 | ], 10 | "compilerOptions": { 11 | "noEmit": true 12 | }, 13 | "files": [] 14 | } 15 | -------------------------------------------------------------------------------- /packages/parjs/jest.config.mjs: -------------------------------------------------------------------------------- 1 | import common from "../../jest.root.mjs"; 2 | 3 | /** @type {import("jest").Config} */ 4 | const config = { 5 | rootDir: ".", 6 | setupFilesAfterEnv: ["/spec/utilities/index.ts"], 7 | ...common 8 | }; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /packages/char-info/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./src/tsconfig.json" 5 | }, 6 | { 7 | "path": "./spec/tsconfig.json" 8 | } 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "noEmit": true 13 | }, 14 | "files": [] 15 | } 16 | -------------------------------------------------------------------------------- /packages/parjs/examples/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./src/tsconfig.json" 5 | }, 6 | { 7 | "path": "./spec/tsconfig.json" 8 | } 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "noEmit": true 13 | }, 14 | "files": [] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "typescript", 6 | "tsconfig": "tsconfig.json", 7 | "option": "watch", 8 | "problemMatcher": ["$tsc-watch"], 9 | "group": "build", 10 | "label": "tsc: watch - tsconfig.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/qthen.spec.ts: -------------------------------------------------------------------------------- 1 | import { string } from "@lib"; 2 | import { qthen } from "@lib/combinators"; 3 | 4 | describe("qthen", () => { 5 | it("succeeds", () => { 6 | const parser = string("ab").pipe(qthen(string("cd"))); 7 | expect(parser.parse("abcd")).toBeSuccessful("cd"); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/thenq.spec.ts: -------------------------------------------------------------------------------- 1 | import { string } from "@lib"; 2 | import { thenq } from "@lib/combinators"; 3 | 4 | describe("thenq", () => { 5 | it("succeeds", () => { 6 | const parser = string("ab").pipe(thenq(string("cd"))); 7 | expect(parser.parse("abcd")).toBeSuccessful("ab"); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /packages/parjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./src/tsconfig.json" 5 | }, 6 | { 7 | "path": "./spec/tsconfig.json" 8 | }, 9 | { 10 | "path": "./examples/tsconfig.json" 11 | } 12 | ], 13 | "compilerOptions": { 14 | "composite": true, 15 | "noEmit": true 16 | }, 17 | "files": [] 18 | } 19 | -------------------------------------------------------------------------------- /packages/parjs/examples/src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "references": [ 4 | { 5 | "path": "../../src/tsconfig.json" 6 | } 7 | ], 8 | "compilerOptions": { 9 | "noEmit": false, 10 | "baseUrl": ".", 11 | "composite": true, 12 | "outDir": "../dist" 13 | }, 14 | "include": ["**/*.ts"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/char-info/spec/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "references": [ 4 | { 5 | "path": "../src/tsconfig.json" 6 | } 7 | ], 8 | "compilerOptions": { 9 | "noEmit": false, 10 | "tsBuildInfoFile": "../dist-spec/tsconfig.tsbuildinfo", 11 | "outDir": "../dist-spec" 12 | }, 13 | "include": ["**/*.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/standalone/state.spec.ts: -------------------------------------------------------------------------------- 1 | import { state } from "@lib"; 2 | 3 | describe("state", () => { 4 | const parser = state(); 5 | const uState = { tag: 1 }; 6 | const someInput = "abcd"; 7 | it("fails on non-empty input", () => { 8 | const parseResult = parser.parse(someInput, uState); 9 | expect(parseResult).toBeFailure(); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinated.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser } from "./parjser"; 2 | import { ParjserBase } from "./parser"; 3 | 4 | export abstract class Combinated extends ParjserBase implements Parjser { 5 | constructor(protected source: CombinatorInput) { 6 | super(); 7 | } 8 | } 9 | 10 | export type CombinatorInput = ParjserBase & Parjser; 11 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/mapConst.ts: -------------------------------------------------------------------------------- 1 | import type { ParjsCombinator } from "../parjser"; 2 | import { map } from "./map"; 3 | 4 | /** 5 | * Applies the source parser and yields the constant value `result`. 6 | * 7 | * @param result The constant value to yield. 8 | */ 9 | export function mapConst(result: T): ParjsCombinator { 10 | return map(() => result); 11 | } 12 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/standalone/result.spec.ts: -------------------------------------------------------------------------------- 1 | import { result } from "@lib"; 2 | 3 | describe("result", () => { 4 | const parser = result("x"); 5 | const noInput = ""; 6 | it("succeeds on empty input", () => { 7 | expect(parser.parse(noInput)).toBeSuccessful("x"); 8 | }); 9 | it("fails on non-empty input", () => { 10 | expect(parser.parse("a")).toBeFailure(); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/char-info/examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "char-info-examples", 3 | "private": true, 4 | "license": "MIT", 5 | "scripts": { 6 | "clean": "shx rm -rf dist" 7 | }, 8 | "dependencies": { 9 | "char-info": "*" 10 | }, 11 | "devDependencies": { 12 | "@types/node": "20.9.1", 13 | "npm-run-all": "^4.1.5", 14 | "ts-node": "^10.9.2", 15 | "typescript": "^5.4.5" 16 | }, 17 | "packageManager": "yarn@4.1.1" 18 | } 19 | -------------------------------------------------------------------------------- /packages/parjs/examples/spec/tuple.spec.ts: -------------------------------------------------------------------------------- 1 | import { surrounded } from "../src/tuple"; 2 | 3 | describe("the tuple example", () => { 4 | describe("a complex example", () => { 5 | const successInput = "(1, 2 , 3 )"; 6 | 7 | const parser = surrounded; 8 | 9 | it("can parse the example", () => { 10 | const result = parser.parse(successInput); 11 | expect(result).toBeSuccessful(); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/parjs/examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parjs-examples", 3 | "private": true, 4 | "scripts": { 5 | "clean": "shx rm -rf dist", 6 | "test": "jest", 7 | "test:coverage": "jest --coverage" 8 | }, 9 | "dependencies": { 10 | "parjs": "*" 11 | }, 12 | "devDependencies": { 13 | "@types/node": "20.9.1", 14 | "npm-run-all": "^4.1.5", 15 | "ts-node": "^10.9.2", 16 | "typescript": "^5.4.5" 17 | }, 18 | "packageManager": "yarn@4.1.1" 19 | } 20 | -------------------------------------------------------------------------------- /packages/parjs/spec/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.base.json", 3 | "references": [ 4 | { 5 | "path": "../src/tsconfig.json" 6 | } 7 | ], 8 | "compilerOptions": { 9 | "composite": false, 10 | "declarationMap": false, 11 | "noEmit": true, 12 | "baseUrl": ".", 13 | "types": ["jest", "node"], 14 | "paths": { 15 | "@lib": ["../src"], 16 | "@lib/*": ["../src/*"] 17 | } 18 | }, 19 | "include": ["**/*.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "CommonJS", 5 | "noImplicitOverride": true, 6 | "target": "ES2020", 7 | "lib": ["ES2020"], 8 | "allowSyntheticDefaultImports": true, 9 | "esModuleInterop": true, 10 | "moduleResolution": "Node", 11 | "resolveJsonModule": true, 12 | "declarationMap": true, 13 | "sourceMap": true, 14 | "noEmit": true, 15 | "strict": true 16 | }, 17 | "include": [] 18 | } 19 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "arrowParens": "avoid", 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "endOfLine": "lf", 7 | 8 | "overrides": [ 9 | { 10 | "files": "*.{json,yml,yaml}", 11 | "options": { 12 | "tabWidth": 2, 13 | "proseWrap": "always" 14 | } 15 | } 16 | ], 17 | "plugins": [ 18 | "prettier-plugin-organize-imports", 19 | "prettier-plugin-packagejson", 20 | "prettier-plugin-jsdoc" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/parjs/examples/spec/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../../tsconfig.base.json", 3 | "references": [ 4 | { 5 | "path": "../src/tsconfig.json" 6 | }, 7 | { 8 | "path": "../../src/tsconfig.json" 9 | } 10 | ], 11 | "compilerOptions": { 12 | "noEmit": true, 13 | "composite": false, 14 | "declarationMap": false, 15 | 16 | "baseUrl": ".", 17 | "paths": { 18 | "@examples/*": ["../src/*"] 19 | } 20 | }, 21 | "include": ["**/*.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/expects.spec.ts: -------------------------------------------------------------------------------- 1 | import { anyChar } from "@lib"; 2 | import type { ParjserBase } from "@lib/internal"; 3 | 4 | describe("expects", () => { 5 | it("is correct", () => { 6 | const base = anyChar().expects("a character") as ParjserBase; 7 | const parser = anyChar().expects("a character of some sort") as ParjserBase; 8 | expect(parser.expecting).toBe("a character of some sort"); 9 | expect(base.expecting).toBe("a character"); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/standalone/position.spec.ts: -------------------------------------------------------------------------------- 1 | import { position } from "@lib"; 2 | 3 | describe("position", () => { 4 | const parser = position(); 5 | const noInput = ""; 6 | it("succeeds on empty input", () => { 7 | const parseResult = parser.parse(noInput); 8 | expect(parseResult).toBeSuccessful(0); 9 | }); 10 | it("fails on non-empty input", () => { 11 | const parseResult = parser.parse("abc"); 12 | expect(parseResult).toBeFailure(); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/parjs/examples/jest.config.mjs: -------------------------------------------------------------------------------- 1 | import common from "../../../jest.root.mjs"; 2 | 3 | /** @type {import("jest").Config} */ 4 | const config = { 5 | rootDir: ".", 6 | setupFilesAfterEnv: ["/../spec/utilities/index.ts"], 7 | ...common, 8 | moduleNameMapper: { 9 | "^@lib/(.*)$": "/../src/$1", 10 | "^@lib$": "/../src", 11 | "^@examples/(.*)$": "/$1", 12 | "^@examples$": "" 13 | } 14 | }; 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/standalone/fail.spec.ts: -------------------------------------------------------------------------------- 1 | import { ResultKind, fail } from "@lib"; 2 | 3 | describe("fail", () => { 4 | const parser = fail({ 5 | kind: "Fatal" 6 | }); 7 | const noInput = ""; 8 | const input = "abc"; 9 | it("fails on no input", () => { 10 | expect(parser.parse(noInput)).toBeFailure(ResultKind.FatalFail); 11 | }); 12 | it("fails on non-empty input", () => { 13 | expect(parser.parse(input)).toBeFailure(ResultKind.FatalFail); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /jest.root.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import("jest").Config} */ 2 | const common = { 3 | moduleNameMapper: { 4 | "^@lib/(.*)$": "/src/$1", 5 | "^@lib$": "/src" 6 | }, 7 | transform: { 8 | "^.+\\.tsx?$": ["@swc/jest"] 9 | }, 10 | testEnvironment: "node", 11 | testMatch: ["/spec/**/*.spec.ts"], 12 | collectCoverageFrom: ["/src/**/*.ts"], 13 | coverageDirectory: "./coverage", 14 | collectCoverage: false, 15 | testPathIgnorePatterns: ["node_modules", "dist"] 16 | }; 17 | 18 | export default common; 19 | -------------------------------------------------------------------------------- /.istanbul.yml: -------------------------------------------------------------------------------- 1 | instrumentation: 2 | root: . 3 | extensions: 4 | - .js 5 | default-excludes: true 6 | excludes: 7 | - "**/*.no-cover.js" 8 | - "**/test/**" #no need to instrument this test code! Things references by it remain covered. 9 | - "**/functions/**" #many indicators are not utilized so let's ignore them. 10 | - "**/implementation/issues.js" #contains things like exception throws, not executable code 11 | - "**/basics/result.js" #should not be instrumented 12 | - ".obsidian/**" #not executable code 13 | - ".github/**" #not executable code 14 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/functions/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Recursively applies join to an array of arrays. 3 | * 4 | * @param arr 5 | */ 6 | type StringOrArray = string | StringOrArray[]; 7 | 8 | export function recJoin(arr: StringOrArray): string { 9 | if (arr instanceof Array) { 10 | return arr.map(x => recJoin(x)).join(""); 11 | } else { 12 | return String(arr); 13 | } 14 | } 15 | 16 | export function padInt(n: number, digits: number, char: string) { 17 | const str = n.toString(); 18 | if (str.length >= digits) return str; 19 | return char.repeat(digits - str.length) + str; 20 | } 21 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/thenPick.spec.ts: -------------------------------------------------------------------------------- 1 | import { ResultKind, anyCharOf, string } from "@lib"; 2 | import { thenPick } from "@lib/combinators"; 3 | 4 | it("thenPick", () => { 5 | const parser = anyCharOf("ab").pipe( 6 | thenPick(x => { 7 | if (x === "a") { 8 | return string("a"); 9 | } else { 10 | return string("b"); 11 | } 12 | }) 13 | ); 14 | 15 | expect(parser.parse("aa")).toBeSuccessful("a"); 16 | expect(parser.parse("bb")).toBeSuccessful("b"); 17 | expect(parser.parse("ab")).toBeFailure(ResultKind.HardFail); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/state.ts: -------------------------------------------------------------------------------- 1 | import { ResultKind } from "../result"; 2 | import type { ParsingState } from "../state"; 3 | 4 | import type { Parjser } from "../parjser"; 5 | import { ParjserBase } from "../parser"; 6 | 7 | class State extends ParjserBase { 8 | type = "state"; 9 | expecting = "expecting anything"; 10 | 11 | _apply(ps: ParsingState): void { 12 | ps.value = ps.userState; 13 | ps.kind = ResultKind.Ok; 14 | } 15 | } 16 | 17 | /** Returns a parser that yields the current user state object. It always succeeds. */ 18 | export function state(): Parjser { 19 | return new State(); 20 | } 21 | -------------------------------------------------------------------------------- /packages/char-info/spec/indicator-test.ts: -------------------------------------------------------------------------------- 1 | import type { Macro } from "ava"; 2 | import test from "ava"; 3 | 4 | type CharCases = { 5 | true: string[]; 6 | false: string[]; 7 | }; 8 | export const charIndicatorTest: Macro<[(x: any) => boolean, CharCases]> = (t, f, cases) => { 9 | for (const cr of cases.true) { 10 | t.true(f(cr), `expected true for ${cr}`); 11 | } 12 | 13 | for (const cr of cases.false) { 14 | t.false(f(cr), `expected false for ${cr}`); 15 | } 16 | }; 17 | 18 | export function defineIndicatorTest(title: string, f: (x: any) => boolean, cases: CharCases) { 19 | test(title, charIndicatorTest, f, cases); 20 | } 21 | -------------------------------------------------------------------------------- /packages/char-info/src/indicator-type.ts: -------------------------------------------------------------------------------- 1 | /** @module char-info */ 2 | 3 | /** Lets you determine if a character or codepoint is part of some Unicode character grouping. */ 4 | export interface CharClassIndicator { 5 | /** 6 | * Checks if the codepoint is part of the Unicode group. 7 | * 8 | * @param char The codepoint. 9 | */ 10 | code(char: number): boolean; 11 | 12 | /** 13 | * Checks if a character is in a Unicode group. 14 | * 15 | * @param str A string representing a character. 16 | */ 17 | char(str: string): boolean; 18 | } 19 | 20 | /** Provides information about */ 21 | export interface CharInfoProvider {} 22 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/exactly.spec.ts: -------------------------------------------------------------------------------- 1 | import { ResultKind, string } from "@lib"; 2 | import { exactly } from "@lib/combinators"; 3 | 4 | describe("exactly combinator", () => { 5 | const parser = string("ab").pipe(exactly(2)); 6 | 7 | it("succeeds with exact matches", () => { 8 | expect(parser.parse("abab")).toBeSuccessful(["ab", "ab"]); 9 | }); 10 | 11 | it("hard fails with 0 < matches <= N", () => { 12 | expect(parser.parse("ab")).toBeFailure(ResultKind.HardFail); 13 | }); 14 | 15 | it("soft fails with matches == 0", () => { 16 | expect(parser.parse("a")).toBeFailure(ResultKind.SoftFail); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/position.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser } from "../parjser"; 2 | import { ParjserBase } from "../parser"; 3 | import { ResultKind } from "../result"; 4 | import type { ParsingState } from "../state"; 5 | 6 | /** 7 | * Returns a parser that succeeds without consuming input and yields the current position as an 8 | * integer. 9 | */ 10 | export function position(): Parjser { 11 | return new (class Position extends ParjserBase { 12 | type = "position"; 13 | expecting = "anything"; 14 | 15 | _apply(ps: ParsingState) { 16 | ps.value = ps.position; 17 | ps.kind = ResultKind.Ok; 18 | } 19 | })(); 20 | } 21 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/standalone/eof.spec.ts: -------------------------------------------------------------------------------- 1 | import { ResultKind, eof } from "@lib"; 2 | import { then } from "@lib/combinators"; 3 | 4 | describe("eof", () => { 5 | const parser = eof(); 6 | const failInput = "a"; 7 | const successInput = ""; 8 | it("success on empty input", () => { 9 | expect(parser.parse(successInput)).toBeSuccessful(undefined); 10 | }); 11 | it("fail on non-empty input", () => { 12 | expect(parser.parse(failInput)).toBeFailure(ResultKind.SoftFail); 13 | }); 14 | it("chain multiple EOF succeeds", () => { 15 | const parser2 = parser.pipe(then(eof())); 16 | expect(parser2.parse("")).toBeSuccessful(undefined); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/parjs/src/combinators.ts: -------------------------------------------------------------------------------- 1 | export { 2 | backtrack, 3 | between, 4 | each, 5 | exactly, 6 | flatten, 7 | later, 8 | many, 9 | many1, 10 | manyBetween, 11 | manySepBy, 12 | manyTill, 13 | map, 14 | mapConst, 15 | maybe, 16 | must, 17 | mustCapture, 18 | not, 19 | or, 20 | pipe, 21 | qthen, 22 | reason, 23 | recover, 24 | replaceState, 25 | stringify, 26 | then, 27 | thenPick, 28 | thenq 29 | } from "./internal/combinators"; 30 | export type { 31 | ArrayWithSeparators, 32 | DelayedParjser, 33 | NestedArray, 34 | ParserFailureState, 35 | RecoveryFunction, 36 | UserStateOrProjection 37 | } from "./internal/combinators"; 38 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/result.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser } from "../parjser"; 2 | import { ParjserBase } from "../parser"; 3 | import { ResultKind } from "../result"; 4 | import type { ParsingState } from "../state"; 5 | 6 | class Result extends ParjserBase { 7 | type = "result"; 8 | expecting = "expecting anything"; 9 | constructor(private value: T) { 10 | super(); 11 | } 12 | 13 | _apply(ps: ParsingState): void { 14 | ps.value = this.value; 15 | ps.kind = ResultKind.Ok; 16 | } 17 | } 18 | 19 | /** 20 | * Returns a parser that succeeds without consuming input and yields the constant `value`. 21 | * 22 | * @param value The value the returned parser will yield. 23 | */ 24 | export function result(value: T): Parjser { 25 | return new Result(value); 26 | } 27 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/rest.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser } from "../parjser"; 2 | import { ParjserBase } from "../parser"; 3 | import { ResultKind } from "../result"; 4 | import type { ParsingState } from "../state"; 5 | 6 | class Rest extends ParjserBase { 7 | type = "rest"; 8 | expecting = "expecting anything"; 9 | 10 | _apply(pr: ParsingState) { 11 | const { position, input } = pr; 12 | const text = input.substring(Math.min(position, input.length)); 13 | pr.position = input.length; 14 | pr.value = text; 15 | pr.kind = ResultKind.Ok; 16 | } 17 | } 18 | 19 | /** 20 | * Returns a parser that consumes all the rest of the input and yields the text that was parsed. 21 | * Always succeeds. 22 | */ 23 | export function rest(): Parjser { 24 | return new Rest(); 25 | } 26 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/expects.spec.ts: -------------------------------------------------------------------------------- 1 | import { fail, nope } from "@lib"; 2 | import { reason, then } from "@lib/combinators"; 3 | 4 | describe("expects combinator", () => { 5 | const base = nope("deez nuts"); 6 | it("sets the expecting", () => { 7 | const parser = base.pipe(reason("imma let you finish")); 8 | 9 | expect(parser.parse("abc")).toMatchObject({ 10 | kind: "Soft", 11 | reason: "imma let you finish" 12 | }); 13 | }); 14 | it("modifies expecting", () => { 15 | const parser = base.pipe( 16 | then(fail("deez nuts")), 17 | reason(x => `${x.reason}! gottem!`) 18 | ); 19 | expect(parser.parse("abc")).toMatchObject({ 20 | kind: "Soft", 21 | reason: "deez nuts! gottem!" 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/parjs/examples/src/tuple.ts: -------------------------------------------------------------------------------- 1 | import { float, whitespace } from "parjs"; 2 | import { between, manySepBy } from "parjs/combinators"; 3 | 4 | // Built-in parser for floating point numbers. 5 | const tupleElement = float(); 6 | 7 | // Allow whitespace around elements: 8 | const paddedElement = tupleElement.pipe(between(whitespace())); 9 | // Multiple instances of {paddedElement}, separated by a comma: 10 | const separated = paddedElement.pipe(manySepBy(",")); 11 | 12 | // Surround everything with parentheses: 13 | export const surrounded = separated.pipe(between("(", ")"), between(whitespace())); 14 | 15 | export function runExample() { 16 | console.log(surrounded.parse("(1, 2 , 3 )")); 17 | 18 | const result = surrounded.parse(` 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | (1, a, 3 28 | 29 | `); 30 | console.log(result.toString()); 31 | } 32 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/issues.ts: -------------------------------------------------------------------------------- 1 | import { ParserDefinitionError } from "../errors"; 2 | 3 | /** Some canned error throwers. */ 4 | export const Issues = { 5 | /** 6 | * Throws an error saying that the parser is about to enter an infinite loop. 7 | * 8 | * @param name 9 | */ 10 | guardAgainstInfiniteLoop(name: string): never { 11 | throw new ParserDefinitionError( 12 | name, 13 | `The combinator '${name}' expected one of its arguments to change the parser state.` 14 | ); 15 | }, 16 | 17 | delayedParserNotInit(name: string): never { 18 | throw new ParserDefinitionError(name, `Delayed parser not initalized.`); 19 | }, 20 | 21 | delayedParserAlreadyInit(): never { 22 | throw new ParserDefinitionError("", `Delayed parser has already been initialized`); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /packages/parjs/src/errors.ts: -------------------------------------------------------------------------------- 1 | import type { ParjsFailure } from "./internal"; 2 | 3 | /** A parent class for all errors thrown by Parjs. */ 4 | export abstract class ParjsError extends Error { 5 | override name = this.constructor.name; 6 | } 7 | 8 | /** An error that is thrown when it is assumed a parser will succeed, but it fails. */ 9 | export class ParjsParsingFailure extends ParjsError { 10 | constructor(public failure: ParjsFailure) { 11 | super(`Expected parsing to succeed, but it failed. Reason: ${failure.trace.reason}`); 12 | } 13 | } 14 | 15 | /** An error thrown to indicate that a parser has been constructed inappropriately. */ 16 | export class ParserDefinitionError extends ParjsError { 17 | constructor( 18 | public parserName: string, 19 | message: string 20 | ) { 21 | super(`${parserName}: ${message}`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/char-info/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { Interval } from "node-interval-tree"; 2 | 3 | export { 4 | AsciiCodes, 5 | isAscii, 6 | isAsciiCode, 7 | isDigit, 8 | isDigitCode, 9 | isHex, 10 | isHexCode, 11 | isLetter, 12 | isLetterCode, 13 | isLower, 14 | isLowerCode, 15 | isNewline, 16 | isNewlineCode, 17 | isSpace, 18 | isSpaceCode, 19 | isUpper, 20 | isUpperCode, 21 | isWordChar, 22 | isWordCharCode 23 | } from "./ascii"; 24 | 25 | export { 26 | UnicodeBlock, 27 | UnicodeCategory, 28 | UnicodeScript, 29 | uniGetBlock, 30 | uniGetCategories, 31 | uniGetScripts, 32 | uniInBlock, 33 | uniInCategory, 34 | uniInScript, 35 | uniIsDecimal, 36 | uniIsLetter, 37 | uniIsLower, 38 | uniIsNewline, 39 | uniIsSpace, 40 | uniIsUpper 41 | } from "./unicode"; 42 | 43 | export type { CharClassIndicator } from "./indicator-type"; 44 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/flatten.ts: -------------------------------------------------------------------------------- 1 | import type { ParjsCombinator } from "../parjser"; 2 | import { map } from "./map"; 3 | 4 | /** The type of an arbitrarily nested array or a non-array element. */ 5 | export type NestedArray = T | NestedArray[]; 6 | 7 | function flattenNestedArrays(arr: NestedArray): T[] { 8 | if (!Array.isArray(arr)) { 9 | return [arr]; 10 | } 11 | const items = [] as T[]; 12 | for (const item of arr) { 13 | if (Array.isArray(item)) { 14 | items.push(...flattenNestedArrays(item)); 15 | } else { 16 | items.push(item); 17 | } 18 | } 19 | return items; 20 | } 21 | 22 | /** 23 | * Applies the source parser and projects its result into a flat array - an array with non-array 24 | * elements. 25 | */ 26 | export function flatten(): ParjsCombinator, T[]> { 27 | return map, T[]>(x => flattenNestedArrays(x)); 28 | } 29 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/eof.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser } from "../parjser"; 2 | import { ParjserBase } from "../parser"; 3 | import { ResultKind } from "../result"; 4 | import type { ParsingState } from "../state"; 5 | 6 | class Eof extends ParjserBase { 7 | type = "eof"; 8 | expecting = "expecting end of input"; 9 | constructor(private result?: T) { 10 | super(); 11 | } 12 | _apply(ps: ParsingState): void { 13 | if (ps.position === ps.input.length) { 14 | ps.kind = ResultKind.Ok; 15 | ps.value = this.result; 16 | } else { 17 | ps.kind = ResultKind.SoftFail; 18 | } 19 | } 20 | } 21 | 22 | /** 23 | * Returns a parser that succeeds if there is no more input. 24 | * 25 | * @param result Optionally, the result the parser will yield. Defaults to undefined. 26 | */ 27 | export function eof(result?: T): Parjser { 28 | return new Eof(result); 29 | } 30 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/many1.spec.ts: -------------------------------------------------------------------------------- 1 | import { ResultKind, fail, string } from "@lib"; 2 | import { many1 } from "@lib/internal/combinators"; 3 | 4 | describe("many1 combinator", () => { 5 | const parser = string("ab").pipe(many1()); 6 | it("succeeds with 1 match", () => { 7 | expect(parser.parse("ab")).toBeSuccessful(["ab"]); 8 | }); 9 | it("succeeds with N>1 matches", () => { 10 | expect(parser.parse("ababab")).toBeSuccessful(["ab", "ab", "ab"]); 11 | }); 12 | it("fails with 0 matches", () => { 13 | expect(parser.parse("")).toBeFailure(ResultKind.SoftFail); 14 | }); 15 | it("fails hard when parser fails hard", () => { 16 | const parser2 = fail().pipe(many1()); 17 | const error = parser2.parse(""); 18 | expect(error).toBeFailure("Hard"); 19 | }); 20 | 21 | it("fails soft when many fails soft on 1st iteration", () => { 22 | expect(parser.parse("a")).toBeFailure(ResultKind.SoftFail); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/backtrack.ts: -------------------------------------------------------------------------------- 1 | import type { ParjsCombinator } from "../../"; 2 | import { Combinated } from "../combinated"; 3 | import type { ParsingState } from "../state"; 4 | import { defineCombinator } from "./combinator"; 5 | 6 | class Backtrack extends Combinated { 7 | type = "backtrack"; 8 | expecting = this.source.expecting; 9 | 10 | _apply(ps: ParsingState): void { 11 | const { position } = ps; 12 | this.source.apply(ps); 13 | if (ps.isOk) { 14 | // if inner succeeded, we backtrack. 15 | ps.position = position; 16 | } 17 | // whatever code ps had, we return it. 18 | } 19 | } 20 | 21 | /** 22 | * Applies the source parser. If it succeeds, backtracks to the current position in the input and 23 | * yields the result. 24 | */ 25 | export function backtrack(): ParjsCombinator { 26 | return defineCombinator(source => { 27 | return new Backtrack(source); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/util-types.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser } from "./parjser"; 2 | import type { ImplicitParjser } from "./wrap-implicit"; 3 | 4 | export type union = Args extends [ 5 | infer A, 6 | infer B, 7 | infer C, 8 | infer D, 9 | infer E, 10 | ...infer Rest 11 | ] 12 | ? A | B | C | D | E | union 13 | : Args extends [infer A, infer B, infer C, infer D, ...infer Rest] 14 | ? A | B | C | D | union 15 | : Args extends [infer A, infer B, infer C, ...infer Rest] 16 | ? A | B | C | union 17 | : Args extends [infer A, infer B, ...infer Rest] 18 | ? A | B | union 19 | : Args extends [infer A, ...infer Rest] 20 | ? A | union 21 | : never; 22 | export type getParsedType> = T extends RegExp 23 | ? string[] 24 | : T extends string 25 | ? T 26 | : T extends Parjser 27 | ? U 28 | : never; 29 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/index.ts: -------------------------------------------------------------------------------- 1 | export { caseString } from "./case-string"; 2 | export { charCodeWhere } from "./char-code-where"; 3 | export { 4 | anyChar, 5 | anyCharOf, 6 | digit, 7 | hex, 8 | letter, 9 | lower, 10 | noCharOf, 11 | space, 12 | spaces1, 13 | uniDecimal, 14 | uniLetter, 15 | uniLower, 16 | uniUpper, 17 | upper, 18 | whitespace 19 | } from "./char-types"; 20 | export { charWhere } from "./char-where"; 21 | export { eof } from "./eof"; 22 | export { fail, nope } from "./fail"; 23 | export { float } from "./float"; 24 | export type { FloatOptions } from "./float"; 25 | export { int } from "./int"; 26 | export type { IntOptions } from "./int"; 27 | export { newline, uniNewline } from "./newline"; 28 | export { position } from "./position"; 29 | export { rest } from "./rest"; 30 | export { result } from "./result"; 31 | export { state } from "./state"; 32 | export { stringLen } from "./string-len"; 33 | export { anyStringOf } from "./string-of"; 34 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/recover.spec.ts: -------------------------------------------------------------------------------- 1 | import { fail, ResultKind, string } from "@lib"; 2 | import { recover, stringify, then } from "@lib/combinators"; 3 | 4 | describe("recover combinator", () => { 5 | const parser = string("a").pipe( 6 | then("b"), 7 | stringify(), 8 | recover(() => ({ kind: "Soft" })) 9 | ); 10 | it("succeeds", () => { 11 | expect(parser.parse("ab")).toBeSuccessful("ab"); 12 | expect(parser.parse("ab")).toBeSuccessful("ab"); 13 | }); 14 | it("fails softly on soft fail", () => { 15 | expect(parser.parse("ba")).toBeFailure(ResultKind.SoftFail); 16 | }); 17 | it("fails softly on hard fail", () => { 18 | expect(parser.parse("a")).toBeFailure(ResultKind.SoftFail); 19 | }); 20 | it("fails fatally on fatal fail", () => { 21 | const parser2 = fail({ 22 | kind: "Fatal" 23 | }).pipe(recover(() => ({ kind: "Soft" }))); 24 | expect(parser2.parse("")).toBeFailure(ResultKind.FatalFail); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2025 Parjs contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/parjs/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2025 Parjs contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/char-info/LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2025 Parjs contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/standalone/later.spec.ts: -------------------------------------------------------------------------------- 1 | import { string } from "@lib"; 2 | import { later } from "@lib/combinators"; 3 | import type { ParjserBase } from "@lib/internal"; 4 | 5 | describe("later", () => { 6 | const internal = string("a") as ParjserBase; 7 | const parser = later(); 8 | 9 | it("throws when not init", () => { 10 | expect(() => later().parse("")).toThrow(); 11 | }); 12 | 13 | parser.init(internal); 14 | 15 | it("throws when double init", () => { 16 | expect(() => parser.init(internal)).toThrow(); 17 | }); 18 | 19 | it("first success", () => { 20 | expect(parser.parse("a")).toBeSuccessful("a"); 21 | }); 22 | 23 | it("second success", () => { 24 | expect(parser.parse("a")).toBeSuccessful("a"); 25 | }); 26 | 27 | it("fail", () => { 28 | expect(parser.parse("")).toBeFailure("Soft"); 29 | }); 30 | 31 | it("expecting after init", () => { 32 | expect((parser as unknown as ParjserBase).expecting).toEqual(internal.expecting); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/not.spec.ts: -------------------------------------------------------------------------------- 1 | import { fail, rest, ResultKind, string } from "@lib"; 2 | import { not, stringify, then } from "@lib/combinators"; 3 | 4 | describe("not combinator", () => { 5 | const parser = string("a").pipe(then("b"), stringify(), not()); 6 | it("succeeds on empty input/soft fail", () => { 7 | expect(parser.parse("")).toBeSuccessful(undefined); 8 | }); 9 | it("succeeds on hard fail if we take care of the rest", () => { 10 | const parser2 = parser.pipe(then(rest())); 11 | expect(parser2.parse("a")).toBeSuccessful(); 12 | }); 13 | it("soft fails on passing input", () => { 14 | expect(parser.parse("ab")).toBeFailure(ResultKind.SoftFail); 15 | }); 16 | it("fails fatally on fatal fail", () => { 17 | const parser2 = fail({ 18 | kind: "Fatal", 19 | reason: "fatal" 20 | }).pipe(not()); 21 | expect(parser2.parse("")).toBeFailure(ResultKind.FatalFail); 22 | }); 23 | it("fails on too much input", () => { 24 | expect(parser.parse("a")).toBeFailure(ResultKind.SoftFail); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /documentation/architecture-design-records/002-switch-testing-framework-to-jest.md: -------------------------------------------------------------------------------- 1 | # Switch testing framework to jest 2 | 3 | Date: 2023-11-19 4 | 5 | ## Context 6 | 7 | We have been using jasmine for testing for a couple of years. The tests have been run on precompiled code (typescript compiled to javascript). Running them has been fast. 8 | 9 | Recently the project has not received updates to its packages. Updating to newer dependency versions has caused some issues with dependencies that are no longer supported. 10 | 11 | ## Decision 12 | 13 | We will move to jest for testing. Jest is a popular testing framework that is actively maintained. It is also used by the react community. 14 | 15 | The new developer / maintainer Mika Vilpas has experience with jest and prefers it over jasmine. 16 | 17 | We will add instructions to the developer documentation on how to run the tests. 18 | 19 | ## Expected consequences 20 | 21 | Jest will be easy to use for many developers. It will also be fast to run. 22 | 23 | Because of its popularity, Jest will have good support for the foreseeable future. 24 | 25 | It will be possible to debug the tests. 26 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/each.ts: -------------------------------------------------------------------------------- 1 | import type { ParsingState } from "../state"; 2 | 3 | import type { ParjsCombinator, ParjsProjection } from "../parjser"; 4 | 5 | import type { CombinatorInput } from "../combinated"; 6 | import { Combinated } from "../combinated"; 7 | import { wrapImplicit } from "../wrap-implicit"; 8 | 9 | class Each extends Combinated { 10 | type = "each"; 11 | expecting = this.source.expecting; 12 | 13 | constructor( 14 | source: CombinatorInput, 15 | private readonly _action: ParjsProjection 16 | ) { 17 | super(source); 18 | } 19 | 20 | _apply(ps: ParsingState): void { 21 | this.source.apply(ps); 22 | if (!ps.isOk) { 23 | return; 24 | } 25 | this._action(ps.value as T, ps.userState); 26 | } 27 | } 28 | 29 | /** 30 | * Applies `action` to each result emitted by the source parser and emits its results unchanged. 31 | * 32 | * @param action 33 | */ 34 | export function each(action: ParjsProjection): ParjsCombinator { 35 | return source => new Each(wrapImplicit(source), action); 36 | } 37 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/string-len.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser } from "../.."; 2 | import { ParjserBase } from "../parser"; 3 | import { ResultKind } from "../result"; 4 | import type { ParsingState } from "../state"; 5 | 6 | class StringLen extends ParjserBase { 7 | type = "stringLen"; 8 | expecting = `expecting ${this.length} characters`; 9 | 10 | constructor(private length: number) { 11 | super(); 12 | } 13 | 14 | _apply(ps: ParsingState) { 15 | const { position, input } = ps; 16 | const length = this.length; 17 | if (input.length < position + length) { 18 | ps.kind = ResultKind.SoftFail; 19 | return; 20 | } 21 | ps.position += length; 22 | ps.value = input.substring(position, position + length); 23 | ps.kind = ResultKind.Ok; 24 | } 25 | } 26 | 27 | /** 28 | * Returns a parser that parses exactly `length` characters and yields the text that was parsed. 29 | * 30 | * @param length The number of characters to parse. 31 | */ 32 | export function stringLen(length: number): Parjser { 33 | return new StringLen(length); 34 | } 35 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/debug.spec.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser } from "@lib"; 2 | import { string } from "@lib"; 3 | 4 | const consoleSpy = jest.spyOn(console, "log").mockImplementation(); 5 | 6 | describe("the 'debug' method", () => { 7 | it("should return a parser with the correct type", () => { 8 | const parser: Parjser<"a"> = string("a").debug(); 9 | expect(parser.type).toBe("string"); 10 | }); 11 | 12 | it("should log the correct message", () => { 13 | const parser: Parjser<"a"> = string("a").expects("an 'a' character").debug(); 14 | parser.parse("a"); 15 | expect(consoleSpy.mock.calls.at(0)?.[0]).toMatchInlineSnapshot(` 16 | "consumed 'a' (length 1) 17 | at position 0->1 18 | 👍🏻 (Ok) 19 | { 20 | "input": "a", 21 | "userState": {}, 22 | "position": 1, 23 | "stack": [], 24 | "value": "a", 25 | "kind": "OK" 26 | } 27 | { 28 | "type": "string", 29 | "expecting": "an 'a' character" 30 | }" 31 | `); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /documentation/architecture-design-records/001-document-decisions.md: -------------------------------------------------------------------------------- 1 | # Document the context of decisions 2 | 3 | Date: 2023-11-19 4 | 5 | > An architecture decision record (ADR) is a document that captures an important architecture decision made along with its context and consequences. 6 | > 7 | > 8 | 9 | The purpose of an ADR is to help future developers understand why a past decision was made. 10 | 11 | ## Context 12 | 13 | ## Decision 14 | 15 | - We will start writing ADRs. The ADRs will be short and to the point. 16 | - The ADRs will be written in Markdown. 17 | - They will be stored in the repository. We will keep outdated ADRs in [outdated](./outdated/) 18 | 19 | ## Expected consequences 20 | 21 | - Future developers will understand why a past decision was made. This will help them understand if the decision is still valid. 22 | - The ADRs will be easy to read and write. They will also be portable. They can be read easily in GitHub where we store our code. 23 | - Because the ADRs are in the repository, they will be easy to find. They will also be easy to update. Git will provide a history of changes. 24 | -------------------------------------------------------------------------------- /packages/parjs/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | ErrorLocation, 3 | FailureInfo, 4 | FloatOptions, 5 | IntOptions, 6 | ParjsCombinator, 7 | ParjsProjection, 8 | ParjsResult, 9 | ParjsValidator, 10 | Parjser, 11 | SuccessInfo, 12 | Trace, 13 | UserState 14 | } from "./internal/index"; 15 | 16 | export { ParjsError, ParjsParsingFailure, ParserDefinitionError } from "./errors"; 17 | export { 18 | ParjsFailure, 19 | ParjsSuccess, 20 | ResultKind, 21 | anyChar, 22 | anyCharOf, 23 | anyStringOf, 24 | caseString, 25 | charCodeWhere, 26 | charWhere, 27 | digit, 28 | eof, 29 | fail, 30 | float, 31 | hex, 32 | int, 33 | letter, 34 | lower, 35 | newline, 36 | noCharOf, 37 | nope, 38 | position, 39 | regexp, 40 | rest, 41 | result, 42 | space, 43 | spaces1, 44 | state, 45 | string, 46 | stringLen, 47 | uniDecimal, 48 | uniLetter, 49 | uniLower, 50 | uniNewline, 51 | uniUpper, 52 | upper, 53 | whitespace 54 | } from "./internal/index"; 55 | export type { ConvertibleScalar, ImplicitParjser } from "./internal/wrap-implicit"; 56 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/not.ts: -------------------------------------------------------------------------------- 1 | import type { ParjsCombinator } from "../../"; 2 | import { Combinated } from "../combinated"; 3 | import { ResultKind } from "../result"; 4 | import type { ParsingState } from "../state"; 5 | import { wrapImplicit } from "../wrap-implicit"; 6 | 7 | class Not extends Combinated { 8 | type = "not"; 9 | expecting = `not expecting: ${this.source.expecting}`; // TODO: better reason 10 | 11 | _apply(ps: ParsingState): void { 12 | const { position } = ps; 13 | this.source.apply(ps); 14 | if (ps.isOk) { 15 | ps.position = position; 16 | ps.kind = ResultKind.SoftFail; 17 | } else if (ps.kind === ResultKind.HardFail || ps.kind === ResultKind.SoftFail) { 18 | // hard fails are okay here 19 | ps.kind = ResultKind.Ok; 20 | ps.position = position; 21 | return; 22 | } 23 | // the remaining case is a fatal failure that isn't recovered from. 24 | } 25 | } 26 | 27 | /** Applies the source parser. Succeeds if if it fails softly, and fails otherwise. */ 28 | export function not(): ParjsCombinator { 29 | return source => new Not(wrapImplicit(source)); 30 | } 31 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/maybe.ts: -------------------------------------------------------------------------------- 1 | import type { CombinatorInput } from "../combinated"; 2 | import { Combinated } from "../combinated"; 3 | import type { ParjsCombinator } from "../parjser"; 4 | import { ResultKind } from "../result"; 5 | import type { ParsingState } from "../state"; 6 | import { wrapImplicit } from "../wrap-implicit"; 7 | 8 | class MaybeCombinator extends Combinated { 9 | type = "maybe"; 10 | 11 | expecting = "expecting anything"; 12 | 13 | constructor( 14 | source: CombinatorInput, 15 | private readonly _val: S | undefined 16 | ) { 17 | super(source); 18 | } 19 | 20 | _apply(ps: ParsingState): void { 21 | this.source.apply(ps); 22 | if (ps.isSoft) { 23 | // on soft failure, set the value and result to OK 24 | ps.value = this._val; 25 | ps.kind = ResultKind.Ok; 26 | } 27 | // on ok/hard/fatal, propagate the result. 28 | } 29 | } 30 | 31 | /** 32 | * Applies the source parser. If it fails softly, succeeds and yields `val`. 33 | * 34 | * @param val 35 | */ 36 | export function maybe(val?: S): ParjsCombinator { 37 | return source => new MaybeCombinator(wrapImplicit(source), val); 38 | } 39 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/index.ts: -------------------------------------------------------------------------------- 1 | export { ParjserBase, ParserUserState } from "./parser"; 2 | export type { ParsingState, UserState } from "./state"; 3 | 4 | export { composeCombinator, defineCombinator } from "./combinators"; 5 | export type { ParjsCombinator, ParjsProjection, ParjsValidator, Parjser } from "./parjser"; 6 | export { regexp, string } from "./parser"; 7 | export { 8 | anyChar, 9 | anyCharOf, 10 | anyStringOf, 11 | caseString, 12 | charCodeWhere, 13 | charWhere, 14 | digit, 15 | eof, 16 | fail, 17 | float, 18 | hex, 19 | int, 20 | letter, 21 | lower, 22 | newline, 23 | noCharOf, 24 | nope, 25 | position, 26 | rest, 27 | result, 28 | space, 29 | spaces1, 30 | state, 31 | stringLen, 32 | uniDecimal, 33 | uniLetter, 34 | uniLower, 35 | uniNewline, 36 | uniUpper, 37 | upper, 38 | whitespace 39 | } from "./parsers"; 40 | export type { FloatOptions, IntOptions } from "./parsers"; 41 | export { ParjsFailure, ParjsSuccess, ResultKind } from "./result"; 42 | export type { ErrorLocation, FailureInfo, ParjsResult, SuccessInfo, Trace } from "./result"; 43 | export { BasicParsingState, FAIL_RESULT, UNINITIALIZED_RESULT } from "./state"; 44 | export { wrapImplicit } from "./wrap-implicit"; 45 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/stringify.ts: -------------------------------------------------------------------------------- 1 | import { Combinated } from "../combinated"; 2 | import { recJoin } from "../functions"; 3 | import type { ParjsCombinator } from "../parjser"; 4 | import type { ParsingState } from "../state"; 5 | import { wrapImplicit } from "../wrap-implicit"; 6 | 7 | class Str extends Combinated { 8 | type = "stringify"; 9 | expecting = this.source.expecting; 10 | 11 | _apply(ps: ParsingState): void { 12 | this.source.apply(ps); 13 | if (!ps.isOk) { 14 | return; 15 | } 16 | const { value } = ps; 17 | const typeStr = typeof value; 18 | if (typeStr === "string") { 19 | return; 20 | } 21 | if (value === null || value === undefined) { 22 | ps.value = String(value); 23 | } else if (value instanceof Array) { 24 | ps.value = recJoin(value); 25 | } else if (typeStr === "symbol") { 26 | ps.value = String(value).slice(7, -1); 27 | } else { 28 | ps.value = value.toString(); 29 | } 30 | } 31 | } 32 | 33 | /** Applies the source parser and yields a stringified result. */ 34 | export function stringify(): ParjsCombinator { 35 | return source => new Str(wrapImplicit(source)); 36 | } 37 | -------------------------------------------------------------------------------- /documentation/architecture-design-records/003-release-stable-version.md: -------------------------------------------------------------------------------- 1 | # Release stable version 2 | 3 | Date: 2024-01-15 4 | 5 | ## Context 6 | 7 | The project version has been on `0.x.x` since its inception. However, the semver.org website states that the major version `0` is intended for "rapid development and fast iteration" only: 8 | 9 | > https://semver.org/#how-do-i-know-when-to-release-100 10 | > 11 | > How do I know when to release 1.0.0? 12 | > 13 | > If your software is being used in production, it should probably already be 1.0.0. If you have a stable API on which users have come to depend, you should be 1.0.0. If you’re worrying a lot about backward compatibility, you should probably already be 1.0.0. 14 | 15 | This is important because many tools for automatic dependency updates, such as dependabot or renovate, are typically configured to not automatically upgrade major version changes (they should be handled manually instead). 16 | 17 | ## Decision 18 | 19 | We will release version `1.0.0` of the project. 20 | 21 | From this point on, we will follow semantic versioning. 22 | 23 | ## Expected consequences 24 | 25 | Users of the project will be able to use automatic dependency updates for the project. 26 | 27 | Releasing breaking changes will not cause issues for users who are using automatic dependency updates. 28 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/between.spec.ts: -------------------------------------------------------------------------------- 1 | import { ResultKind, float, string } from "@lib"; 2 | import { between } from "@lib/combinators"; 3 | 4 | describe("the between combinators", () => { 5 | describe("two argument version", () => { 6 | const parser = string("a").pipe(between("(", string(")"))); 7 | it("succeeds", () => { 8 | expect(parser.parse("(a)")).toBeSuccessful("a"); 9 | }); 10 | it("fails soft if first between fails", () => { 11 | expect(parser.parse("[a)")).toBeFailure(ResultKind.SoftFail); 12 | }); 13 | it("fails hard if middle/last fails", () => { 14 | expect(parser.parse("(b)")).toBeFailure(ResultKind.HardFail); 15 | expect(parser.parse("(b]")).toBeFailure(ResultKind.HardFail); 16 | }); 17 | }); 18 | describe("one argument version", () => { 19 | const parser = string("a").pipe(between("!")); 20 | it("succeeds", () => { 21 | expect(parser.parse("!a!")).toBeSuccessful("a"); 22 | }); 23 | }); 24 | 25 | describe("two argument version with different types", () => { 26 | const parser = string("a").pipe(between("_", float())); 27 | it("succeeds", () => { 28 | expect(parser.parse("_a3.14")).toBeSuccessful("a"); 29 | }); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/later.ts: -------------------------------------------------------------------------------- 1 | import type { ParsingState } from "../state"; 2 | 3 | import { Issues } from "../issues"; 4 | import type { Parjser } from "../parjser"; 5 | import { ParjserBase } from "../parser"; 6 | 7 | /** A parser with logic to be determined later. Useful for defining some kinds of recursive parsers. */ 8 | export interface DelayedParjser extends Parjser { 9 | init(resolved: Parjser): void; 10 | } 11 | 12 | class Late extends ParjserBase implements DelayedParjser { 13 | type = "later"; 14 | _resolved!: ParjserBase; 15 | 16 | get expecting() { 17 | return !this._resolved ? "unbound delayed parser" : this._resolved.expecting; 18 | } 19 | 20 | init(resolved: Parjser) { 21 | if (this._resolved) Issues.delayedParserAlreadyInit(); 22 | this._resolved = resolved as ParjserBase; 23 | } 24 | 25 | _apply(ps: ParsingState): void { 26 | if (!this._resolved) { 27 | Issues.delayedParserNotInit(""); 28 | } 29 | this._resolved.apply(ps); 30 | } 31 | } 32 | 33 | /** 34 | * Returns a parser that has no logic by itself and must be initialized with another parser by 35 | * calling the parser's `init` function. 36 | */ 37 | export function later(): DelayedParjser { 38 | return new Late(); 39 | } 40 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/index.ts: -------------------------------------------------------------------------------- 1 | export { backtrack } from "./backtrack"; 2 | export { between } from "./between"; 3 | export { composeCombinator, defineCombinator, pipe } from "./combinator"; 4 | export { each } from "./each"; 5 | export { exactly } from "./exactly"; 6 | export { flatten } from "./flatten"; 7 | export type { NestedArray } from "./flatten"; 8 | export { later } from "./later"; 9 | export type { DelayedParjser } from "./later"; 10 | export { many } from "./many"; 11 | export { manySepBy } from "./many-sep-by"; 12 | export type { ArrayWithSeparators } from "./many-sep-by"; 13 | export { manyBetween, manyTill } from "./many-till"; 14 | export { many1 } from "./many1"; 15 | export { map } from "./map"; 16 | export { mapConst } from "./mapConst"; 17 | export { maybe } from "./maybe"; 18 | export { must } from "./must"; 19 | export { mustCapture } from "./must-capture"; 20 | export { not } from "./not"; 21 | export { or } from "./or"; 22 | export { reason } from "./reason"; 23 | export { recover } from "./recover"; 24 | export type { ParserFailureState, RecoveryFunction } from "./recover"; 25 | export { replaceState } from "./replace-state"; 26 | export type { UserStateOrProjection } from "./replace-state"; 27 | export { stringify } from "./stringify"; 28 | export { qthen, then, thenq } from "./then"; 29 | export { thenPick } from "./then-pick"; 30 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/between.ts: -------------------------------------------------------------------------------- 1 | import type { ImplicitParjser, ParjsCombinator } from "../../index"; 2 | import { wrapImplicit } from "../wrap-implicit"; 3 | import { defineCombinator } from "./combinator"; 4 | import { qthen, thenq } from "./then"; 5 | 6 | /** 7 | * Applies `pre`, the source parser, and then `post`. Yields the result of the source parser. 8 | * 9 | * @param pre The parser to precede the source. 10 | * @param post The parser to proceed the source. 11 | */ 12 | export function between( 13 | pre: ImplicitParjser, 14 | post: ImplicitParjser 15 | ): ParjsCombinator; 16 | /** 17 | * Applies the `surrounding` parser, followed by the source parser, and then another instance of 18 | * `surrounding`. Yields the result of the source parser. 19 | * 20 | * @param surrounding The parser to apply before and after the source. 21 | */ 22 | export function between(surrounding: ImplicitParjser): ParjsCombinator; 23 | export function between( 24 | implPre: ImplicitParjser, 25 | implPost?: ImplicitParjser 26 | ): ParjsCombinator { 27 | const pre = wrapImplicit(implPre); 28 | const post = implPost ? wrapImplicit(implPost) : pre; 29 | return defineCombinator(source => { 30 | return pre.pipe(qthen(source), thenq(post)); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/numeric-helpers.ts: -------------------------------------------------------------------------------- 1 | import { AsciiCodes, isDigitCode } from "char-info/ascii"; 2 | import type { ParsingState } from "../state"; 3 | 4 | /** Parsing helper. */ 5 | export class NumericHelpersType { 6 | parseDigitsInBase(ps: ParsingState, base: number) { 7 | let { position } = ps; 8 | const { input } = ps; 9 | const length = input.length; 10 | const result = 0; 11 | for (; position < length; position++) { 12 | const curCode = input.charCodeAt(position); 13 | if (!isDigitCode(curCode, base)) { 14 | break; 15 | } 16 | } 17 | ps.position = position; 18 | return result; 19 | } 20 | 21 | /** 22 | * Tries to parse a '+' or '-'. Returns the sign that was parsed, or 0 if the parsing failed. 23 | * 24 | * @param ps 25 | * @returns {number} 26 | */ 27 | parseSign(ps: ParsingState) { 28 | let sign = 0; 29 | const curChar = ps.input.charCodeAt(ps.position); 30 | if (curChar === AsciiCodes.minus) { 31 | sign = -1; 32 | ps.position++; 33 | } else if (curChar === AsciiCodes.plus) { 34 | ps.position++; 35 | sign = 1; 36 | } 37 | return sign; 38 | } 39 | } 40 | 41 | export const NumericHelpers = new NumericHelpersType(); 42 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/must.ts: -------------------------------------------------------------------------------- 1 | import type { ParjsCombinator } from "../../"; 2 | import type { CombinatorInput } from "../combinated"; 3 | import { Combinated } from "../combinated"; 4 | import type { ParjsValidator } from "../parjser"; 5 | import type { ParsingState } from "../state"; 6 | import { wrapImplicit } from "../wrap-implicit"; 7 | 8 | class Must extends Combinated { 9 | type = "must"; 10 | expecting = `internal parser ${this.source.type} yielding a result satisfying condition`; 11 | constructor( 12 | source: CombinatorInput, 13 | private readonly _predicate: ParjsValidator 14 | ) { 15 | super(source); 16 | } 17 | _apply(ps: ParsingState): void { 18 | this.source.apply(ps); 19 | if (!ps.isOk) { 20 | return; 21 | } 22 | const result = this._predicate(ps.value as T, ps.userState); 23 | if (result === true) return; 24 | ps.kind = result.kind || "Soft"; 25 | ps.reason = result.reason || "failed to fulfill a predicate"; 26 | } 27 | } 28 | 29 | /** 30 | * Applies the source parser and makes sure its result fulfills `predicate`. 31 | * 32 | * @param predicate The condition to check for. 33 | */ 34 | export function must(predicate: ParjsValidator): ParjsCombinator { 35 | return source => new Must(wrapImplicit(source), predicate); 36 | } 37 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/map.ts: -------------------------------------------------------------------------------- 1 | import { Combinated } from "../combinated"; 2 | import type { ParjsCombinator, ParjsProjection } from "../parjser"; 3 | import type { ParjserBase } from "../parser"; 4 | import type { ParsingState } from "../state"; 5 | import { wrapImplicit } from "../wrap-implicit"; 6 | 7 | class Map extends Combinated { 8 | type = "map"; 9 | expecting = this.source.expecting; 10 | constructor( 11 | source: ParjserBase, 12 | private readonly _projection: ParjsProjection 13 | ) { 14 | super(source); 15 | } 16 | _apply(ps: ParsingState): void { 17 | this.source.apply(ps); 18 | if (!ps.isOk) { 19 | return; 20 | } 21 | ps.value = this._projection(ps.value as T, ps.userState); 22 | } 23 | } 24 | 25 | /** 26 | * Applies the source parser and projects its result with `projection`. 27 | * 28 | * @param projection The projection to apply. 29 | */ 30 | export function map(projection: ParjsProjection): ParjsCombinator { 31 | return source => new Map(wrapImplicit(source), projection); 32 | } 33 | 34 | /** 35 | * Applies the source parser and yields the constant value `result`. 36 | * 37 | * @param result The constant value to yield. 38 | */ 39 | export function mapConst(result: T): ParjsCombinator { 40 | return map(() => result); 41 | } 42 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/char-where.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser, ParjsValidator } from "../parjser"; 2 | import { ParjserBase } from "../parser"; 3 | import { ResultKind } from "../result"; 4 | import type { ParsingState } from "../state"; 5 | 6 | class CharWhere extends ParjserBase { 7 | type = "charWhere"; 8 | expecting = "expecting a character matching a predicate"; 9 | constructor(private predicate: ParjsValidator) { 10 | super(); 11 | } 12 | _apply(ps: ParsingState): void { 13 | const { position, input } = ps; 14 | const predicate = this.predicate; 15 | if (position >= input.length) { 16 | ps.kind = ResultKind.SoftFail; 17 | ps.reason = "expecting a character"; 18 | return; 19 | } 20 | const curChar = input[position]; 21 | const result = predicate(curChar, ps.userState); 22 | if (result !== true) { 23 | ps.reason = result.reason || this.expecting; 24 | ps.kind = result.kind || "Soft"; 25 | return; 26 | } 27 | ps.value = curChar; 28 | ps.position++; 29 | ps.kind = ResultKind.Ok; 30 | } 31 | } 32 | 33 | /** 34 | * Returns a parser that parses a single character fulfilling `predicate`. 35 | * 36 | * @param predicate The predicate the character has to fulfill. 37 | */ 38 | export function charWhere(predicate: ParjsValidator): Parjser { 39 | return new CharWhere(predicate); 40 | } 41 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/fail.ts: -------------------------------------------------------------------------------- 1 | /** @module parjs */ 2 | import { defaults } from "../../utils"; 3 | import type { Parjser } from "../parjser"; 4 | import { ParjserBase } from "../parser"; 5 | import type { FailureInfo } from "../result"; 6 | import type { ParsingState } from "../state"; 7 | 8 | const defaultFailure: FailureInfo = { 9 | kind: "Hard", 10 | reason: "User initiated failure." 11 | }; 12 | 13 | /** 14 | * Returns a parser that fails softly with a given reason. 15 | * 16 | * @param reason The reason for the failure. 17 | */ 18 | export function nope(reason: string): Parjser { 19 | return fail({ 20 | kind: "Soft", 21 | reason 22 | }); 23 | } 24 | 25 | class Fail extends ParjserBase { 26 | type = "fail"; 27 | expecting = this.failure.reason; 28 | constructor(private failure: FailureInfo) { 29 | super(); 30 | } 31 | _apply(ps: ParsingState): void { 32 | ps.kind = this.failure.kind; 33 | ps.reason = this.expecting; 34 | } 35 | } 36 | 37 | /** 38 | * Returns a parser that will always fail with the given failure info. 39 | * 40 | * @param pFailure How the parser should fail. 41 | */ 42 | export function fail(pFailure?: Partial | string): Parjser { 43 | const failure = 44 | typeof pFailure === "string" 45 | ? ({ kind: "Hard", reason: pFailure } as const) 46 | : defaults(pFailure, defaultFailure); 47 | return new Fail(failure); 48 | } 49 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/then-pick.ts: -------------------------------------------------------------------------------- 1 | import type { CombinatorInput } from "../combinated"; 2 | import { Combinated } from "../combinated"; 3 | import type { ParjsCombinator, ParjsProjection } from "../parjser"; 4 | import type { ParsingState } from "../state"; 5 | import type { ImplicitParjser } from "../wrap-implicit"; 6 | import { wrapImplicit } from "../wrap-implicit"; 7 | 8 | class ThenPick extends Combinated { 9 | type = "then-pick"; 10 | expecting = `${this.source.expecting} then `; 11 | constructor( 12 | source: CombinatorInput, 13 | private _ctor: ParjsProjection> 14 | ) { 15 | super(source); 16 | } 17 | _apply(ps: ParsingState): void { 18 | this.source.apply(ps); 19 | if (!ps.isOk) { 20 | return; 21 | } 22 | const nextParser = wrapImplicit(this._ctor(ps.value as A, ps.userState)); 23 | nextParser.apply(ps); 24 | if (ps.isOk) { 25 | return; 26 | } 27 | if (ps.isSoft) { 28 | ps.kind = "Hard"; 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * Applies the source parser, and then applies a selector on the source parser's result and user 35 | * state to choose or create the parser to apply next. 36 | * 37 | * @param selector 38 | */ 39 | export function thenPick( 40 | selector: ParjsProjection> 41 | ): ParjsCombinator { 42 | return source => new ThenPick(wrapImplicit(source), selector); 43 | } 44 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/exactly.ts: -------------------------------------------------------------------------------- 1 | import type { ImplicitParjser } from "../../index"; 2 | import type { CombinatorInput } from "../combinated"; 3 | import { Combinated } from "../combinated"; 4 | import { ResultKind } from "../result"; 5 | import type { ParsingState } from "../state"; 6 | import { wrapImplicit } from "../wrap-implicit"; 7 | 8 | class Exactly extends Combinated { 9 | type = "exactly"; 10 | expecting = this.source.expecting; 11 | 12 | constructor( 13 | source: CombinatorInput, 14 | private count: number 15 | ) { 16 | super(source); 17 | } 18 | 19 | _apply(ps: ParsingState): void { 20 | const arr = [] as unknown[]; 21 | for (let i = 0; i < this.count; i++) { 22 | this.source.apply(ps); 23 | if (!ps.isOk) { 24 | if (ps.kind === ResultKind.SoftFail && i > 0) { 25 | ps.kind = ResultKind.HardFail; 26 | } 27 | // fail because the inner parser has failed. 28 | return; 29 | } 30 | arr.push(ps.value); 31 | } 32 | ps.value = arr; 33 | } 34 | } 35 | 36 | /** 37 | * Applies the source parser exactly `count` times, and yields all the results in an array. 38 | * 39 | * @param count The number of times to apply the source parser. 40 | */ 41 | export function exactly(count: number) { 42 | return (source: ImplicitParjser) => { 43 | return new Exactly(wrapImplicit(source), count); 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/char-code-where.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser, ParjsValidator } from "../parjser"; 2 | import { ParjserBase } from "../parser"; 3 | import { ResultKind } from "../result"; 4 | import type { ParsingState } from "../state"; 5 | 6 | class CharCodeWhere extends ParjserBase { 7 | type = "charCodeWhere"; 8 | expecting = "expecting a character matching a predicate"; 9 | 10 | constructor(private predicate: ParjsValidator) { 11 | super(); 12 | } 13 | 14 | _apply(ps: ParsingState): void { 15 | const { position, input } = ps; 16 | const predicate = this.predicate; 17 | if (position >= input.length) { 18 | ps.kind = ResultKind.SoftFail; 19 | ps.reason = "expecting a character"; 20 | return; 21 | } 22 | const curChar = input.charCodeAt(position); 23 | const result = predicate(curChar, ps.userState); 24 | if (result !== true) { 25 | ps.kind = result.kind || "Soft"; 26 | ps.reason = result.reason || "expecting a character matching a predicate"; 27 | return; 28 | } 29 | ps.value = String.fromCharCode(curChar); 30 | ps.position++; 31 | ps.kind = ResultKind.Ok; 32 | } 33 | } 34 | 35 | /** 36 | * Returns a parser that parses one character, and checks its code fulfills `predicate`. 37 | * 38 | * @param predicate 39 | */ 40 | export function charCodeWhere(predicate: ParjsValidator): Parjser { 41 | return new CharCodeWhere(predicate); 42 | } 43 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/wrap-implicit.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser } from "./parjser"; 2 | 3 | import type { CombinatorInput } from "./combinated"; 4 | import { regexp, string } from "./parser"; 5 | 6 | /** A {@link Parjser} or a literal value convertible to a {@link Parjser}. */ 7 | /** 8 | * @private Should Not be used from user code. Used to implement implicit parser literals. 9 | * @type {symbol} 10 | */ 11 | export const convertibleSymbol: unique symbol = Symbol("ParjsConvertibleLiteral"); 12 | 13 | /** 14 | * A literal type which is implicitly convertible to a parser. This normally includes the `string` 15 | * and `RegExp` types. 16 | */ 17 | export interface ConvertibleScalar { 18 | [convertibleSymbol](): Parjser; 19 | } 20 | 21 | declare global { 22 | interface String { 23 | [convertibleSymbol](): Parjser; 24 | } 25 | 26 | interface RegExp { 27 | [convertibleSymbol](): Parjser; 28 | } 29 | } 30 | 31 | /** 32 | * Either a Parjser or a scalar value convertible to one. 33 | * 34 | * @module parjs 35 | */ 36 | export type ImplicitParjser = Parjser | ConvertibleScalar; 37 | 38 | export function wrapImplicit(scalarOrParjser: ImplicitParjser): CombinatorInput { 39 | if (typeof scalarOrParjser === "string") { 40 | return string(scalarOrParjser) as unknown as CombinatorInput; 41 | } else if (scalarOrParjser instanceof RegExp) { 42 | return regexp(scalarOrParjser) as CombinatorInput; 43 | } else { 44 | return scalarOrParjser as CombinatorInput; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/must-capture.ts: -------------------------------------------------------------------------------- 1 | import type { FailureInfo } from "../result"; 2 | import type { ParsingState } from "../state"; 3 | 4 | import type { ParjsCombinator } from "../parjser"; 5 | import type { ParjserBase } from "../parser"; 6 | 7 | import { defaults } from "../../utils"; 8 | import { Combinated } from "../combinated"; 9 | import { wrapImplicit } from "../wrap-implicit"; 10 | 11 | const defaultFailure: FailureInfo = { 12 | reason: "succeeded without capturing input", 13 | kind: "Hard" 14 | }; 15 | 16 | class MustCapture extends Combinated { 17 | type = "mustCapture"; 18 | expecting = `expecting internal parser ${this.source.type} to consume input`; 19 | constructor( 20 | source: ParjserBase, 21 | private readonly _failure: FailureInfo 22 | ) { 23 | super(source); 24 | } 25 | 26 | _apply(ps: ParsingState) { 27 | const { position } = ps; 28 | this.source.apply(ps); 29 | if (!ps.isOk) { 30 | return; 31 | } 32 | if (position === ps.position) { 33 | ps.kind = this._failure.kind; 34 | ps.reason = this._failure.reason; 35 | } 36 | } 37 | } 38 | 39 | /** 40 | * Applies the source parser and makes sure it captured some input. 41 | * 42 | * @param pFailure The failure info. 43 | */ 44 | export function mustCapture(pFailure?: Partial): ParjsCombinator { 45 | const failure = defaults(pFailure, defaultFailure); 46 | return source => new MustCapture(wrapImplicit(source), failure); 47 | } 48 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true, 8 | "**/Thumbs.db": true, 9 | "**/*.aux": true, 10 | "**/*.bbl": true, 11 | "**/*.fdb_latexmk": true, 12 | "**/*.fls": true, 13 | "**/*.log": true, 14 | "**/*.out": true, 15 | "**/*.synctex.gz": true, 16 | "**/.idea": true, 17 | "**/.mypy_cache": true, 18 | "**/__pycache__": true, 19 | "**/dist": true, 20 | "**/dist_aux": true, 21 | ".nyc_output": true, 22 | "coverage": true, 23 | "docs": true 24 | }, 25 | "[css]": { 26 | "editor.defaultFormatter": "esbenp.prettier-vscode" 27 | }, 28 | "[html]": { 29 | "editor.defaultFormatter": "esbenp.prettier-vscode" 30 | }, 31 | "[javascript]": { 32 | "editor.defaultFormatter": "esbenp.prettier-vscode" 33 | }, 34 | "[json]": { 35 | "editor.defaultFormatter": "esbenp.prettier-vscode" 36 | }, 37 | "[jsonc]": { 38 | "editor.defaultFormatter": "esbenp.prettier-vscode" 39 | }, 40 | "[scss]": { 41 | "editor.defaultFormatter": "esbenp.prettier-vscode" 42 | }, 43 | "[typescript]": { 44 | "editor.defaultFormatter": "esbenp.prettier-vscode" 45 | }, 46 | "[yaml]": { 47 | "editor.defaultFormatter": "esbenp.prettier-vscode" 48 | }, 49 | "typescript.preferences.preferTypeOnlyAutoImports": true, 50 | "typescript.suggest.autoImports": true, 51 | "typescript.suggest.includeCompletionsForImportStatements": true, 52 | "typescript.updateImportsOnFileMove.enabled": "always" 53 | } 54 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/anyChar.spec.ts: -------------------------------------------------------------------------------- 1 | import type { ParjsFailure, ParjsSuccess } from "@lib"; 2 | import { ResultKind, anyChar } from "@lib"; 3 | 4 | describe("anyChar", () => { 5 | const parser = anyChar(); 6 | const successInput = "a"; 7 | const tooMuchInput = "ab"; 8 | const failInput = ""; 9 | const uniqueState = {}; 10 | it("single char input success", () => { 11 | const result = parser.parse(successInput, uniqueState) as ParjsSuccess; 12 | expect(result).toBeSuccessful("a"); 13 | }); 14 | it("empty input failure", () => { 15 | const result = parser.parse(failInput, uniqueState) as ParjsFailure; 16 | expect(result).toBeFailure(ResultKind.SoftFail); 17 | }); 18 | 19 | it("fails on too much input", () => { 20 | const result = parser.parse(tooMuchInput); 21 | expect(result).toBeFailure(ResultKind.SoftFail); 22 | }); 23 | 24 | describe("resolve", () => { 25 | it("throws", () => { 26 | expect(() => parser.parse("").value).toThrow(); 27 | }); 28 | it("doesn't throw", () => { 29 | expect(parser.parse("a").value).toBe("a"); 30 | }); 31 | }); 32 | 33 | describe("non-string inputs", () => { 34 | it("throws on null, undefined", () => { 35 | expect(() => parser.parse(null as never)).toThrow(); 36 | expect(() => parser.parse(undefined as never)).toThrow(); 37 | }); 38 | it("throws on non-string", () => { 39 | expect(() => parser.parse(5 as never)).toThrow(); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/maybe.spec.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser } from "@lib"; 2 | import { fail, ResultKind, string } from "@lib"; 3 | import { maybe, qthen, stringify, then } from "@lib/combinators"; 4 | 5 | describe("maybe combinator", () => { 6 | it("works", () => { 7 | const p = string("a"); 8 | const m = p.pipe(maybe()); 9 | expect(m.parse("a")).toBeSuccessful(); 10 | expect(m.parse("")).toBeSuccessful(); 11 | }); 12 | 13 | it("causes progress on success", () => { 14 | const p = string("abc").pipe(maybe(), qthen("123")); 15 | expect(p.parse("abc123")).toBeSuccessful("123"); 16 | }); 17 | 18 | it("propagates hard failure", () => { 19 | const p = fail().pipe(maybe()); 20 | expect(p.parse("")).toBeFailure(ResultKind.HardFail); 21 | }); 22 | }); 23 | 24 | describe("maybe combinator again", () => { 25 | const parser = string("a").pipe(then("b"), stringify(), maybe("c")); 26 | 27 | const p2: Parjser<[0 | "a", "b"]> = string("a").pipe(maybe(0), then(string("b"))); 28 | it("succeeds to parse", () => { 29 | expect(parser.parse("ab")).toBeSuccessful("ab"); 30 | }); 31 | 32 | it("if first fails hard, then fail hard", () => { 33 | expect(parser.parse("ax")).toBeFailure(ResultKind.HardFail); 34 | }); 35 | 36 | it("if first fail soft, then return value", () => { 37 | expect(parser.parse("")).toBeSuccessful("c"); 38 | }); 39 | 40 | it("falsy alt value", () => { 41 | const result = p2.parse("b"); 42 | expect(result).toBeSuccessful([0, "b"]); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/char-info/src/indicators.ts: -------------------------------------------------------------------------------- 1 | import type { Interval } from "node-interval-tree"; 2 | import type { CharClassIndicator } from "./indicator-type"; 3 | import type { UnicodeCharGroup } from "./unicode-lookup"; 4 | 5 | function binarySearchInIntervals(intervals: Interval[]) { 6 | return function bin(start: number, end: number, char: number) { 7 | if (start > end) { 8 | return false; 9 | } 10 | const mid = (start + end) >> 1; 11 | const midInterval = intervals[mid]; 12 | if (midInterval.low > char) { 13 | return bin(start, mid - 1, char); 14 | } 15 | if (midInterval.high < char) { 16 | return bin(mid + 1, end, char); 17 | } 18 | return true; 19 | }; 20 | } 21 | 22 | /** Basic implementation for the CharClassIndicator, using binary search in an array of ranges. */ 23 | export class BasicCharClassIndicator implements CharClassIndicator { 24 | _binarySearchInIntervals: (start: number, end: number, char: number) => boolean; 25 | 26 | constructor(private _group: UnicodeCharGroup) { 27 | const intervals = _group.intervals; 28 | this._binarySearchInIntervals = binarySearchInIntervals(intervals); 29 | this.char = this.char.bind(this); 30 | this.code = this.code.bind(this); 31 | } 32 | 33 | code(char: number) { 34 | const intervals = this._group.intervals; 35 | return this._binarySearchInIntervals(0, intervals.length - 1, char); 36 | } 37 | 38 | char(char: string) { 39 | if (char === "") return false; 40 | return this.code(char.codePointAt(0)!); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.markdownlint-cli2.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "MD001": true, 4 | "MD003": { "style": "atx" }, 5 | "MD004": { "style": "dash" }, 6 | "MD005": true, 7 | "MD007": { "indent": 2 }, 8 | "MD009": { "strict": true }, 9 | "MD010": { "code_blocks": false, "spaces_per_tab": 2 }, 10 | "MD011": true, 11 | "MD012": { "maximum": 1 }, 12 | "MD013": { 13 | "line_length": 1000, 14 | "heading_line_length": 80, 15 | "code_blocks": false, 16 | "tables": false 17 | }, 18 | "MD018": true, 19 | "MD019": true, 20 | "MD022": { "lines_above": 0, "lines_below": 0 }, 21 | "MD023": true, 22 | "MD024": false, 23 | "MD025": false, 24 | "MD026": { "punctuation": ",:.;" }, 25 | "MD027": true, 26 | "MD028": true, 27 | "MD029": { "style": "one_or_ordered" }, 28 | "MD030": true, 29 | "MD031": true, 30 | "MD032": true, 31 | "MD034": true, 32 | "MD035": { "style": "---" }, 33 | "MD037": true, 34 | "MD038": true, 35 | "MD039": true, 36 | "MD040": true, 37 | "MD042": true, 38 | "MD045": true, 39 | "MD046": { "style": "fenced" }, 40 | "MD047": true, 41 | "MD048": { "style": "backtick" }, 42 | "MD049": { "style": "asterisk" }, 43 | "MD050": { "style": "asterisk" } 44 | }, 45 | "fix": true, 46 | "globs": [ 47 | "README.md", 48 | "documentation/**/*.md", 49 | "packages/*/README.md", 50 | "packages/*/examples/README.md" 51 | ] 52 | } 53 | -------------------------------------------------------------------------------- /packages/parjs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This project follows semantic versioning. 4 | 5 | ## 1.0.0 6 | 7 | ### Breaking changes 8 | 9 | - fix: the `maybe` combinator's return type changed from returning `T` to `T | undefined`. This type is more correct because `maybe` can be called without an argument. 10 | - refactor: (internal) remove internal namespaces with flat types. `ResultKind.Fail` is now called `ResultKindFail` and so on. 11 | - refactor: added additional type safety (some details available in [a1e925bd782a714c28e1fa49ec2cb2792a80ab88](https://github.com/GregRos/parjs/commit/a1e925bd782a714c28e1fa49ec2cb2792a80ab88)) 12 | 13 | ### New features 14 | 15 | - add `isParjsSuccess`, `isParjsFailure`, `isParjsResult`. They can be used when testing your parsers. 16 | - add example parser for `.ini` files (see the [README](./README.md)) 17 | - add example parser for math expressions (see the [README](./README.md)) 18 | - add the `.many1()` combinator, which is like `.many()` but requires at least one match 19 | - add `.debug()` method for any parser. It will log the parser's name, the input, the result, and the remaining input. This is useful for debugging. See the [README](./README.md) for more details. 20 | - add constant type inference for many parsers and combinators. For example, `const p: Parjser<"a"> = string("a")` is now the default inferred type when using `string()`. Previously, the type would be inferred as `Parjser`. See the [README](./README.md) for more details. 21 | 22 | ### Miscellaneous changes 23 | 24 | - failure stack traces are printed on multiple lines 25 | - the `or()` combinator shows a clearer error message when it fails 26 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/case-string.ts: -------------------------------------------------------------------------------- 1 | /** Copyright © 2024, Oracle and/or its affiliates. */ 2 | /** @module parjs */ 3 | 4 | import type { Parjser } from ".."; 5 | import { ParjserBase } from "../parser"; 6 | import { ResultKind } from "../result"; 7 | import type { ParsingState } from "../state"; 8 | 9 | class CaseInsensitiveString extends ParjserBase { 10 | expecting = `expecting '${this.str}' (case insensitive)`; 11 | type = "string"; 12 | constructor(private str: string) { 13 | super(); 14 | } 15 | 16 | _apply(ps: ParsingState): void { 17 | const { position, input } = ps; 18 | const { str } = this; 19 | if (position + str.length > input.length) { 20 | ps.kind = ResultKind.SoftFail; 21 | return; 22 | } 23 | // This should create a StringSlice object instead of actually 24 | // copying a whole string. 25 | const substr = input.slice(position, position + str.length); 26 | 27 | // Equality test is very very fast. 28 | if (substr.toLowerCase() !== str.toLowerCase()) { 29 | ps.kind = ResultKind.SoftFail; 30 | return; 31 | } 32 | ps.position += str.length; 33 | ps.value = substr; 34 | ps.kind = ResultKind.Ok; 35 | } 36 | } 37 | 38 | /** 39 | * Returns a parser that will parse the string `str` insensitive to its case and yield the text that 40 | * was parsed. If it can't, it will fail softly without consuming input. 41 | * 42 | * @param str The string to parse case insensitively. 43 | */ 44 | export function caseString(str: string): Parjser { 45 | return new CaseInsensitiveString(str); 46 | } 47 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/string-of.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser } from "../parjser"; 2 | import { ParjserBase } from "../parser"; 3 | import { ResultKind } from "../result"; 4 | import type { ParsingState } from "../state"; 5 | 6 | class StringOf extends ParjserBase { 7 | type = "anyStringOf"; 8 | expecting = `expecting any of ${this.strs.map(x => `'${x}'`).join(", ")}`; 9 | 10 | constructor(private strs: string[]) { 11 | super(); 12 | } 13 | 14 | _apply(ps: ParsingState) { 15 | const { position, input } = ps; 16 | const { strs } = this; 17 | for (let i = 0; i < strs.length; i++) { 18 | const curStr = strs[i]; 19 | if (input.length - position < curStr.length) continue; 20 | const curSubstr = input.slice(position, position + curStr.length); 21 | if (curSubstr === curStr) { 22 | // this means we did not contiue strLoop so curStr passed our tests 23 | ps.position = position + curStr.length; 24 | ps.value = curStr; 25 | ps.kind = ResultKind.Ok; 26 | return; 27 | } 28 | } 29 | ps.kind = ResultKind.SoftFail; 30 | } 31 | } 32 | 33 | /** 34 | * Returns a parser that will parse any of the strings in `strs` and yield the text that was parsed. 35 | * If it can't, it will fail softly without consuming input. 36 | * 37 | * @param strs A set of string options to parse. In typescript, you can also use a constant tuple if 38 | * you pass it in using the spread operator (`...`). 39 | */ 40 | export function anyStringOf(...strs: T[]): Parjser { 41 | return new StringOf(strs); 42 | } 43 | -------------------------------------------------------------------------------- /packages/char-info/spec/ascii.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isAscii, 3 | isDigit, 4 | isHex, 5 | isLetter, 6 | isLower, 7 | isNewline, 8 | isSpace, 9 | isUpper, 10 | isWordChar 11 | } from "../dist"; 12 | import { defineIndicatorTest } from "./indicator-test"; 13 | 14 | defineIndicatorTest("isLetter", isLetter, { 15 | true: ["G", "g"], 16 | false: ["4", ""] 17 | }); 18 | 19 | defineIndicatorTest("isLower", isLower, { 20 | true: ["a", "c"], 21 | false: ["4", "", "A"] 22 | }); 23 | 24 | defineIndicatorTest("isUpper", isUpper, { 25 | true: ["A", "C"], 26 | false: ["4", "c"] 27 | }); 28 | 29 | defineIndicatorTest("isSpace", isSpace, { 30 | true: ["\t", " "], 31 | false: ["G", "\n", "\r", "", "4"] 32 | }); 33 | 34 | defineIndicatorTest("isDigit - default base", (x: string) => isDigit(x), { 35 | true: ["4", "2", "9", "0"], 36 | false: ["g", "", "G", "a", "-"] 37 | }); 38 | 39 | defineIndicatorTest("isDigit - base 4", (x: string) => isDigit(x, 4), { 40 | true: ["3", "0"], 41 | false: ["", "4", "a", "9"] 42 | }); 43 | 44 | defineIndicatorTest("isDigit - base 13", (x: string) => isDigit(x, 13), { 45 | true: ["3", "c"], 46 | false: ["d", "f", ""] 47 | }); 48 | 49 | defineIndicatorTest("isHex", isHex, { 50 | true: ["0", "1", "a", "f"], 51 | false: ["g", "z", " ", ""] 52 | }); 53 | 54 | defineIndicatorTest("isWord", isWordChar, { 55 | true: ["a", "4", "_", "-"], 56 | false: ["", "+", " ", "("] 57 | }); 58 | 59 | defineIndicatorTest("isNewline", isNewline, { 60 | true: ["\r", "\n"], 61 | false: [" ", "", "a"] 62 | }); 63 | 64 | defineIndicatorTest("isAscii", isAscii, { 65 | true: ["a", "1", "-", "_", ";", "~", " "], 66 | false: ["Ṛ", "¶", "Ä", "Ÿ", "", "ѥ"] 67 | }); 68 | -------------------------------------------------------------------------------- /.github/workflows/pr.yaml: -------------------------------------------------------------------------------- 1 | name: monorepo PR 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | node-version: 12 | - 22.13.0 13 | steps: 14 | - name: CHECKOUT CODE 15 | uses: actions/checkout@v4 16 | 17 | - name: SETUP COREPACK 18 | run: corepack enable 19 | 20 | - name: SETUP NODE.JS ${{ matrix.node-version }} 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: "22.13.0" 24 | cache: yarn 25 | registry-url: "https://registry.yarnpkg.com" 26 | 27 | - name: SETUP YARN 28 | run: yarn set version stable 29 | 30 | - name: INSTALL 31 | run: yarn install --immutable 32 | 33 | - name: BUILD 34 | run: yarn workspaces foreach -Rpt --from '{char-info,parjs}' run build 35 | 36 | - name: LINT 37 | run: yarn workspaces foreach -Rpt --from '{char-info,parjs}' run lint:check 38 | 39 | - name: TEST 40 | run: yarn workspaces foreach -Rpt --from '{char-info,parjs}' run test:coverage 41 | 42 | - name: Upload coverage reports to Codecov with GitHub Action 43 | uses: codecov/codecov-action@v4.2.0 44 | env: 45 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 46 | with: 47 | plugin: noop 48 | directory: packages/char-info/coverage 49 | flags: char-info 50 | 51 | - name: Upload coverage reports to Codecov with GitHub Action 52 | uses: codecov/codecov-action@v4.2.0 53 | env: 54 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 55 | with: 56 | directory: packages/parjs/coverage 57 | flags: parjs 58 | plugin: noop 59 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/standalone/unicode.spec.ts: -------------------------------------------------------------------------------- 1 | import { ResultKind, uniDecimal, uniLetter, uniNewline } from "@lib"; 2 | import { many } from "@lib/combinators"; 3 | 4 | describe("unicode strings", () => { 5 | describe("uniNewline", () => { 6 | const allNewlines = "\r\r\n\n\u0085\u2028\u2029"; 7 | it("success on all newline string, incl unicode newline", () => { 8 | const unicodeNewline = uniNewline().pipe(many()); 9 | const result = unicodeNewline.parse(allNewlines); 10 | expect(result.kind).toBe(ResultKind.Ok); 11 | if (result.kind !== ResultKind.Ok) return; 12 | expect(result.value.length).toBe(allNewlines.length - 1); 13 | }); 14 | }); 15 | describe("uniLetter", () => { 16 | const pl = uniLetter(); 17 | it("parse hebrew success", () => { 18 | expect(pl.parse("ש")).toBeSuccessful("ש"); 19 | }); 20 | it("parse english success", () => { 21 | expect(pl.parse("a")).toBeSuccessful("a"); 22 | }); 23 | it("parse symbol fail", () => { 24 | expect(pl.parse(":")).toBeFailure(ResultKind.SoftFail); 25 | }); 26 | it("parse digit fail", () => { 27 | expect(pl.parse("5")).toBeFailure(ResultKind.SoftFail); 28 | }); 29 | }); 30 | 31 | describe("uniDecimal", () => { 32 | const pd = uniDecimal(); 33 | it("succeeds on w-arabic", () => { 34 | expect(pd.parse("4")).toBeSuccessful("4"); 35 | }); 36 | it("succeeds on e-arabic", () => { 37 | expect(pd.parse("١")).toBeSuccessful("١"); 38 | }); 39 | it("fails on letter", () => { 40 | expect(pd.parse("a")).toBeFailure(ResultKind.SoftFail); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/standalone/int.spec.ts: -------------------------------------------------------------------------------- 1 | import { ResultKind, int, rest } from "@lib"; 2 | import { thenq } from "@lib/combinators"; 3 | 4 | describe("int parser", () => { 5 | describe("default settings", () => { 6 | const parser = int({ 7 | base: 10, 8 | allowSign: true 9 | }); 10 | it("fails for empty input", () => { 11 | expect(parser.parse("")).toBeFailure(ResultKind.SoftFail); 12 | }); 13 | it("fails for bad digits", () => { 14 | expect(parser.parse("a")).toBeFailure(ResultKind.SoftFail); 15 | }); 16 | it("succeeds for sequence of with sign digits", () => { 17 | expect(parser.parse("-24")).toBeSuccessful(-24); 18 | }); 19 | it("succeeds for sequence of digits without sign", () => { 20 | expect(parser.parse("24")).toBeSuccessful(24); 21 | }); 22 | it("fails for extra letters", () => { 23 | expect(parser.parse("22a")).toBeFailure(ResultKind.SoftFail); 24 | }); 25 | it("chains into rest", () => { 26 | expect(parser.pipe(thenq(rest())).parse("22a")).toBeSuccessful(22); 27 | }); 28 | it("fails hard if there are no digits after sign", () => { 29 | expect(parser.parse("+a")).toBeFailure(ResultKind.HardFail); 30 | }); 31 | }); 32 | describe("no sign", () => { 33 | const parser = int({ 34 | base: 16, 35 | allowSign: false 36 | }); 37 | it("fails for sign start", () => { 38 | expect(parser.parse("-f")).toBeFailure(ResultKind.SoftFail); 39 | }); 40 | it("succeeds without sign, higher base", () => { 41 | expect(parser.parse("f")).toBeSuccessful(15); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/char-info/src/names/categories.ts: -------------------------------------------------------------------------------- 1 | /** @module char-info/unicode */ 2 | /* tslint:disable:naming-convention */ 3 | /** Unicode category names. */ 4 | export namespace UnicodeCategory { 5 | export const Letter = "L"; 6 | export const LetterUppercase = "Lu"; 7 | export const LetterLowercase = "Ll"; 8 | export const LetterTitlecase = "Lt"; 9 | export const LetterModifier = "Lm"; 10 | export const LetterOther = "Lo"; 11 | 12 | export const MarkNonspacing = "Mn"; 13 | export const MarkSpacingCombining = "Mc"; 14 | export const MarkEncloding = "Me"; 15 | export const Mark = "M"; 16 | 17 | export const NumberDecimalDigit = "Nd"; 18 | export const NumberLetter = "Nl"; 19 | export const NumberOther = "No"; 20 | export const Number = "N"; 21 | 22 | export const PunctuationConnector = "Pc"; 23 | export const PunctuationDash = "Pd"; 24 | export const PuncutationOpen = "Ps"; 25 | export const PunctuationClose = "Pe"; 26 | export const PunctuationInitialQuote = "Pi"; 27 | export const PunctuationFinalQuote = "Pf"; 28 | export const PunctuationOther = "Po"; 29 | export const Punctuation = "P"; 30 | 31 | export const SymbolMath = "Sm"; 32 | export const SymbolCurrency = "Sc"; 33 | export const SymbolModifier = "Sk"; 34 | export const SymbolOther = "So"; 35 | export const Symbol = "S"; 36 | 37 | export const SeparatorSpace = "Zs"; 38 | export const SeparatorLine = "Zl"; 39 | export const SeparatorParagraph = "Zp"; 40 | export const Separator = "Z"; 41 | 42 | export const Custom_SeparatorVertical = "_Zv"; 43 | 44 | export const OtherControl = "Cc"; 45 | export const OtherFormat = "Cf"; 46 | export const OtherSurrogate = "Cs"; 47 | export const OtherPrivateUse = "Co"; 48 | export const OtherNotAssigned = "Cn"; 49 | export const Other = "C"; 50 | } 51 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/manyBetween.spec.ts: -------------------------------------------------------------------------------- 1 | import type { UserState } from "@lib"; 2 | import { ResultKind, string } from "@lib"; 3 | import { manyBetween } from "@lib/combinators"; 4 | 5 | describe("the manyBetween combinator", () => { 6 | describe("without a projection function", () => { 7 | it("succeeds when all parsers succeed", () => { 8 | const parser = string("a").pipe(manyBetween("(", ")")); 9 | expect(parser.parse("(aaa)")).toBeSuccessful(["a", "a", "a"]); 10 | }); 11 | 12 | it("fails softly when the source parser fails softly", () => { 13 | const parser = string("a").pipe(manyBetween("(", ")")); 14 | expect(parser.parse("a")).toBeFailure(ResultKind.SoftFail); 15 | }); 16 | 17 | it("defaults the 'till' parser to 'start'", () => { 18 | const parser = string("a").pipe(manyBetween("_")); 19 | expect(parser.parse("_a_")).toBeSuccessful(["a"]); 20 | }); 21 | }); 22 | 23 | describe("with a projection function", () => { 24 | const projection = (results: string[], till: string, state: UserState): string => { 25 | return [...results, till].join(","); 26 | }; 27 | const parser = string("a").pipe(manyBetween("(", ")", projection)); 28 | 29 | it("passes results to the projection function", () => { 30 | expect(parser.parse("(aaa)")).toBeSuccessful("a,a,a,)"); 31 | }); 32 | }); 33 | 34 | it("success", () => { 35 | const res = string("ab") 36 | .pipe( 37 | manyBetween("'", "'", (sources, till, state) => { 38 | return { sources, till, state }; 39 | }) 40 | ) 41 | .parse("'abab'"); 42 | expect(res.kind).toEqual("OK"); 43 | expect(res.value.sources).toEqual(["ab", "ab"]); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/must.spec.ts: -------------------------------------------------------------------------------- 1 | import { ResultKind, eof, string, stringLen } from "@lib"; 2 | import { must, mustCapture, or, stringify, then } from "@lib/combinators"; 3 | 4 | describe("must combinators", () => { 5 | describe("must combinator", () => { 6 | const parser = stringLen(3).pipe( 7 | must( 8 | s => 9 | s === "abc" || { 10 | kind: "Fatal" 11 | } 12 | ) 13 | ); 14 | it("fails softly if original fails softly", () => { 15 | expect(parser.parse("a")).toBeFailure(ResultKind.SoftFail); 16 | }); 17 | it("succeeds if original succeeds and matches condition", () => { 18 | expect(parser.parse("abc")).toBeSuccessful("abc"); 19 | }); 20 | it("fails accordingly if it doesn't match the condition", () => { 21 | expect(parser.parse("abd")).toBeFailure(ResultKind.FatalFail); 22 | }); 23 | }); 24 | 25 | describe("mustCapture combinator", () => { 26 | const parser = string("a").pipe( 27 | then("b"), 28 | stringify(), 29 | or(eof("")), 30 | mustCapture({ 31 | kind: "Fatal" 32 | }) 33 | ); 34 | it("succeeds if it captures", () => { 35 | expect(parser.parse("ab")).toBeSuccessful("ab"); 36 | }); 37 | it("fails softly if original fails softly", () => { 38 | expect(parser.parse("ba")).toBeFailure(ResultKind.SoftFail); 39 | }); 40 | it("fails hard if original fails hard", () => { 41 | expect(parser.parse("ax")).toBeFailure(ResultKind.HardFail); 42 | }); 43 | it("fails accordingly if it succeeds but doesn't capture", () => { 44 | expect(parser.parse("")).toBeFailure(ResultKind.FatalFail); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/newline.ts: -------------------------------------------------------------------------------- 1 | import { AsciiCodes, uniIsNewline } from "char-info"; 2 | import type { Parjser } from "../parjser"; 3 | import { ParjserBase } from "../parser"; 4 | import { ResultKind } from "../result"; 5 | import type { ParsingState } from "../state"; 6 | 7 | class Newline extends ParjserBase { 8 | type = "newline"; 9 | expecting = "expecting newline"; 10 | 11 | constructor(private _unicodeRecognizer?: (x: number) => boolean) { 12 | super(); 13 | } 14 | 15 | _apply(ps: ParsingState) { 16 | const { position, input } = ps; 17 | if (position >= input.length) { 18 | ps.kind = ResultKind.SoftFail; 19 | return; 20 | } 21 | 22 | const pair = input.slice(position, position + 2); 23 | 24 | if (pair === "\r\n") { 25 | ps.position += 2; 26 | ps.value = pair; 27 | ps.kind = ResultKind.Ok; 28 | return; 29 | } 30 | const firstChar = pair.charCodeAt(0); 31 | if ( 32 | firstChar === AsciiCodes.newline || 33 | firstChar === AsciiCodes.carriageReturn || 34 | this._unicodeRecognizer?.(firstChar) 35 | ) { 36 | ps.position++; 37 | ps.value = pair[0]; 38 | ps.kind = ResultKind.Ok; 39 | return; 40 | } 41 | ps.kind = ResultKind.SoftFail; 42 | } 43 | } 44 | 45 | /** 46 | * Parses an ASCII newline, which can be a single character or the sequence `\r\n`. Yields the text 47 | * that was parsed. 48 | */ 49 | export function newline(): Parjser { 50 | return new Newline(); 51 | } 52 | 53 | /** 54 | * Parses a Unicode newline, which includes ASCII newline strings as well as other vertical 55 | * separators such as PARAGRAPH SEPARATOR. 56 | */ 57 | export function uniNewline(): Parjser { 58 | return new Newline(uniIsNewline.code); 59 | } 60 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/special.spec.ts: -------------------------------------------------------------------------------- 1 | import { eof, string } from "@lib"; 2 | import { backtrack, each, map, replaceState, then } from "@lib/combinators"; 3 | 4 | describe("special combinators", () => { 5 | describe("backtrack", () => { 6 | const parser = string("hi").pipe(then(eof()), backtrack()); 7 | 8 | it("fails soft if inner fails soft", () => { 9 | expect(parser.parse("x")).toBeFailure("Soft"); 10 | }); 11 | 12 | it("fails hard if inner fails hard", () => { 13 | expect(parser.parse("hiAQ")).toBeFailure("Hard"); 14 | }); 15 | 16 | it("succeeds if inner succeeds, non-zero match", () => { 17 | const parseHi = string("hi"); 18 | const redundantParser = parseHi.pipe(backtrack(), then("his")); 19 | expect(redundantParser.parse("his")).toBeSuccessful(["hi", "his"]); 20 | }); 21 | }); 22 | 23 | describe("isolate state", () => { 24 | it("works", () => { 25 | const parser = string("hi").pipe( 26 | each((_x, u) => { 27 | // innerState is set inside isolation 28 | expect(u.innerState).toBe(1); 29 | 30 | // outerStste unset inside insolation 31 | expect(u.outerState).toBeUndefined(); 32 | }), 33 | replaceState({ innerState: 1 }), 34 | map((_x, u) => { 35 | // outerState set inside isolation 36 | expect(u.outerState).toBe(1); 37 | 38 | // innerState unset inside isolation 39 | expect(u.innerState).toBeUndefined(); 40 | return u; 41 | }) 42 | ); 43 | 44 | const result = parser.parse("hi", { 45 | outerState: 1 46 | }).value; 47 | expect(result.outerState).toBe(1); 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /documentation/contributing.md: -------------------------------------------------------------------------------- 1 | # Development instructions for contributing to Parjs 2 | 3 | This project is using [Yarn](https://yarnpkg.com/) as a package manager. The recommendation is to install the latest version of Yarn globally on your system. The version of yarn used for development is specified in the package.json file. The latest versions of yarn will choose the correct version to use automatically. 4 | 5 | Common development commands: 6 | 7 | ```sh 8 | # use the correct version of node (defined in .nvmrc) 9 | nvm use 10 | 11 | # install yarn if not installed 12 | # https://yarnpkg.com/getting-started/install 13 | 14 | # install dependencies 15 | yarn 16 | 17 | # typecheck and build 18 | yarn build 19 | 20 | # test 21 | yarn test 22 | 23 | # quality checks 24 | yarn lint:check 25 | yarn lint 26 | yarn lint:fix 27 | ``` 28 | 29 | ## Testing and debugging 30 | 31 | This section is meant for developers who are working on the project and want a workflow for testing and debugging. 32 | 33 | ### On the CLI 34 | 35 | ```sh 36 | # run tests in watch mode (reruns the tests when files change) 37 | yarn test --watch 38 | 39 | # run tests once 40 | yarn test 41 | ``` 42 | 43 | To limit the tests that are run, you can configure the watcher to only run tests that match a pattern. 44 | 45 | You can also make changes to the code to only run tests you want, or to ignore tests you don't want to run. 46 | 47 | ```ts 48 | // to only run some tests: 49 | describe.only("...", () => {}); // or fdescribe 50 | it.only("...", () => {}); // or fit 51 | 52 | // to ignore some tests: 53 | describe.skip("...", () => {}); // or xdescribe 54 | it.skip("...", () => {}); // or xit 55 | ``` 56 | 57 | ### In VSCode 58 | 59 | The recommended way to run tests in VSCode is to use the . It will automatically run tests when you save a file. It will also show you the test results in the editor. 60 | 61 | The extension will allow you to run and debug a single test or a group of tests. You can also run all tests. 62 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/many.ts: -------------------------------------------------------------------------------- 1 | import type { ImplicitParjser, ParjsCombinator } from "../../index"; 2 | import { Issues } from "../issues"; 3 | import { ResultKind } from "../result"; 4 | import type { ParsingState } from "../state"; 5 | 6 | import type { CombinatorInput } from "../combinated"; 7 | import { Combinated } from "../combinated"; 8 | import { wrapImplicit } from "../wrap-implicit"; 9 | 10 | class Many extends Combinated { 11 | type = "many"; 12 | expecting = this.source.expecting; 13 | 14 | constructor( 15 | source: CombinatorInput, 16 | private readonly _maxIterations: number 17 | ) { 18 | super(source); 19 | } 20 | 21 | _apply(ps: ParsingState): void { 22 | let { position } = ps; 23 | const { source, _maxIterations } = this; 24 | const arr = [] as unknown[]; 25 | let i = 0; 26 | for (;;) { 27 | source.apply(ps); 28 | if (!ps.isOk) break; 29 | if (i >= _maxIterations) break; 30 | if (_maxIterations === Infinity && ps.position === position) { 31 | Issues.guardAgainstInfiniteLoop("many"); 32 | } 33 | position = ps.position; 34 | arr.push(ps.value); 35 | i++; 36 | } 37 | if (ps.atLeast(ResultKind.HardFail)) { 38 | return; 39 | } 40 | ps.value = arr; 41 | // recover from the last failure. 42 | ps.position = position; 43 | ps.kind = ResultKind.Ok; 44 | } 45 | } 46 | 47 | /** 48 | * Applies the source parser until it fails softly, and yields all of its results in an array. 49 | * 50 | * @param maxIterations Optionally, the maximum number of times to apply the source parser. Defaults 51 | * to `Infinity`. 52 | */ 53 | export function many(maxIterations = Infinity): ParjsCombinator { 54 | return (source: ImplicitParjser) => { 55 | return new Many(wrapImplicit(source), maxIterations); 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/manyTill.spec.ts: -------------------------------------------------------------------------------- 1 | import type { UserState } from "@lib"; 2 | import { ResultKind, string } from "@lib"; 3 | import { manyBetween, manyTill } from "@lib/combinators"; 4 | 5 | describe("the manyTill combinator", () => { 6 | const parser = string("a").pipe(manyTill("b")); 7 | 8 | it("succeeds when the given parser succeeds", () => { 9 | expect(parser.parse("aaab")).toBeSuccessful(["a", "a", "a"]); 10 | }); 11 | 12 | it("fails softly if original fails softly", () => { 13 | expect(parser.parse("c")).toBeFailure(ResultKind.SoftFail); 14 | }); 15 | 16 | it("fails hard if the 'till' parser fails", () => { 17 | expect(parser.parse("ax")).toBeFailure(ResultKind.HardFail); 18 | }); 19 | }); 20 | 21 | describe("the manyBetween combinator", () => { 22 | describe("without a projection function", () => { 23 | it("succeeds when all parsers succeed", () => { 24 | const parser = string("a").pipe(manyBetween("(", ")")); 25 | expect(parser.parse("(aaa)")).toBeSuccessful(["a", "a", "a"]); 26 | }); 27 | 28 | it("fails softly when the source parser fails softly", () => { 29 | const parser = string("a").pipe(manyBetween("(", ")")); 30 | expect(parser.parse("a")).toBeFailure(ResultKind.SoftFail); 31 | }); 32 | 33 | it("defaults the 'till' parser to 'start'", () => { 34 | const parser = string("a").pipe(manyBetween("_")); 35 | expect(parser.parse("_a_")).toBeSuccessful(["a"]); 36 | }); 37 | }); 38 | 39 | describe("with a projection function", () => { 40 | const projection = (results: string[], till: string, state: UserState): string => { 41 | return [...results, till].join(","); 42 | }; 43 | const parser = string("a").pipe(manyBetween("(", ")", projection)); 44 | 45 | it("passes results to the projection function", () => { 46 | expect(parser.parse("(aaa)")).toBeSuccessful("a,a,a,)"); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/many1.ts: -------------------------------------------------------------------------------- 1 | import { Combinated } from "../combinated"; 2 | import { Issues } from "../issues"; 3 | import type { ParjsCombinator } from "../parjser"; 4 | import type { ParjserBase } from "../parser"; 5 | import { ResultKind } from "../result"; 6 | import type { ParsingState } from "../state"; 7 | import { wrapImplicit } from "../wrap-implicit"; 8 | 9 | class Many1 extends Combinated { 10 | type = "many1"; 11 | expecting = this.source.expecting; 12 | constructor( 13 | source: ParjserBase, 14 | private _maxIterations: number 15 | ) { 16 | super(source); 17 | } 18 | _apply(ps: ParsingState): void { 19 | let { position } = ps; 20 | const arr = [] as unknown[]; 21 | let i = 0; 22 | for (;;) { 23 | this.source.apply(ps); 24 | if (!ps.isOk) break; 25 | if (i >= this._maxIterations) break; 26 | if (this._maxIterations === Infinity && ps.position === position) { 27 | Issues.guardAgainstInfiniteLoop(this.type); 28 | } 29 | position = ps.position; 30 | arr.push(ps.value); 31 | i++; 32 | } 33 | if (ps.atLeast(ResultKind.HardFail)) { 34 | return; 35 | } 36 | 37 | if (i === 0) { 38 | ps.kind = ResultKind.SoftFail; 39 | ps.reason = "expected at least one match"; 40 | return; 41 | } 42 | 43 | ps.value = arr; 44 | // recover from the last failure. 45 | ps.position = position; 46 | ps.kind = ResultKind.Ok; 47 | } 48 | } 49 | 50 | /** 51 | * Applies the source parser 1 or more times until it fails softly. Yields all of its results in an 52 | * array. 53 | * 54 | * @param maxIterations Optionally, the maximum number of times to apply the source parser. Defaults 55 | * to `Infinity`. 56 | */ 57 | export function many1(maxIterations = Infinity): ParjsCombinator { 58 | return source => new Many1(wrapImplicit(source), maxIterations); 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/parjs.push.yaml: -------------------------------------------------------------------------------- 1 | name: publish parjs 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - "packages/parjs/**" 8 | - ".github/workflows/parjs.push.yaml" 9 | - "yaml.lock" 10 | - ".*" 11 | workflow_dispatch: 12 | 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: false 21 | 22 | jobs: 23 | build_deploy: 24 | environment: 25 | name: github-pages 26 | url: ${{ steps.deployment.outputs.page_url }} 27 | runs-on: ubuntu-latest 28 | strategy: 29 | matrix: 30 | node-version: 31 | - 22.13.0 32 | env: 33 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | steps: 35 | - name: CHECKOUT CODE 36 | uses: actions/checkout@v4 37 | 38 | - name: SETUP COREPACK 39 | run: corepack enable 40 | 41 | - name: SETUP NODE.JS ${{ matrix.node-version }} 42 | uses: actions/setup-node@v3 43 | with: 44 | node-version: "22.13.0" 45 | cache: yarn 46 | registry-url: "https://registry.yarnpkg.com" 47 | 48 | - name: SETUP YARN 49 | run: yarn set version stable 50 | 51 | - name: INSTALL 52 | run: yarn install --immutable 53 | 54 | - name: BUILD 55 | run: yarn workspaces foreach -Rpt --from '{parjs,char-info}' run build 56 | 57 | - name: LINT 58 | run: yarn workspace parjs run lint:check 59 | 60 | - name: TEST 61 | run: yarn workspace parjs run test:coverage 62 | 63 | - name: Upload coverage reports to Codecov with GitHub Action 64 | uses: codecov/codecov-action@v4.2.0 65 | env: 66 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 67 | with: 68 | directory: packages/parjs/coverage 69 | flags: parjs 70 | - name: SETUP PUBLISH TOKEN 71 | run: 'echo "npmAuthToken: ${NPM_TOKEN}" >> ~/.yarnrc.yml' 72 | 73 | - name: PUBLISH PARJS PACKAGE 74 | working-directory: packages/parjs 75 | run: yarn npm publish --access public 76 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/replace-state.ts: -------------------------------------------------------------------------------- 1 | import type { ParjsCombinator, UserState } from "../../index"; 2 | import { defaults } from "../../utils"; 3 | import { Combinated } from "../combinated"; 4 | import type { ParjserBase } from "../parser"; 5 | import { ParserUserState } from "../parser"; 6 | import type { ParsingState } from "../state"; 7 | import { wrapImplicit } from "../wrap-implicit"; 8 | 9 | /** A user state object or a projection to the external user state. */ 10 | export type UserStateOrProjection = UserState | ((externalState: UserState) => UserState); 11 | 12 | class IsolateState extends Combinated { 13 | type = "replaceState"; 14 | expecting = this.source.expecting; 15 | constructor( 16 | source: ParjserBase, 17 | private readonly innerStateOrCtor: UserStateOrProjection 18 | ) { 19 | super(source); 20 | } 21 | _apply(ps: ParsingState): void { 22 | const state = ps.userState; 23 | const { innerStateOrCtor, source } = this; 24 | if (typeof innerStateOrCtor === "function") { 25 | ps.userState = defaults(new ParserUserState(), innerStateOrCtor(state)); 26 | } else { 27 | ps.userState = defaults(new ParserUserState(), innerStateOrCtor); 28 | } 29 | source.apply(ps); 30 | ps.userState = state; 31 | } 32 | } 33 | 34 | /** 35 | * When the source parser is applied, the user state will be switched for a different object. After 36 | * it has finished, the previous user state will be restored. This effectively isolates the source 37 | * parser's user state. 38 | * 39 | * If the given paramter is a function, it will be called on the pre-existing user state to 40 | * determine the new user state. If it's a non-fuction object, it will be used as the user state 41 | * instead. 42 | * 43 | * @param innerStateOrCtor The new internal user state or a projection on the existing user state. 44 | */ 45 | export function replaceState(innerStateOrCtor: UserStateOrProjection): ParjsCombinator { 46 | return source => new IsolateState(wrapImplicit(source), innerStateOrCtor); 47 | } 48 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/or.spec.ts: -------------------------------------------------------------------------------- 1 | import type { ParjsFailure } from "@lib"; 2 | import { fail, ResultKind, string } from "@lib"; 3 | import { mapConst, or } from "@lib/combinators"; 4 | 5 | describe("or combinator", () => { 6 | describe("loud or loud", () => { 7 | const parser = string("ab").pipe(or("cd")); 8 | it("succeeds parsing 1st option", () => { 9 | expect(parser.parse("ab")).toBeSuccessful("ab"); 10 | }); 11 | it("suceeds parsing 2nd option", () => { 12 | expect(parser.parse("cd")).toBeSuccessful("cd"); 13 | }); 14 | it("fails parsing both", () => { 15 | expect(parser.parse("ef")).toBeFailure(ResultKind.SoftFail); 16 | }); 17 | it("fails hard when 1st fails hard", () => { 18 | const parser2 = fail({ 19 | reason: "fail", 20 | kind: ResultKind.HardFail 21 | }).pipe(mapConst("x"), or("ab")); 22 | expect(parser2.parse("ab")).toBeFailure(ResultKind.HardFail); 23 | }); 24 | const parser2 = string("ab").pipe( 25 | or( 26 | fail({ 27 | reason: "x", 28 | kind: "Hard" 29 | }) 30 | ) 31 | ); 32 | it("succeeds with 2nd would've failed hard", () => { 33 | expect(parser2.parse("ab")).toBeSuccessful("ab"); 34 | }); 35 | it("fails when 2nd fails hard", () => { 36 | expect(parser2.parse("cd")).toBeFailure(ResultKind.HardFail); 37 | }); 38 | it("reason includes all expecting", () => { 39 | const reasons = ["it broke", "nope", "not this", "not that"]; 40 | const allFails = reasons.map(x => 41 | fail({ 42 | reason: x, 43 | kind: "Soft" 44 | }) 45 | ); 46 | const parser3 = allFails[0].pipe(or(allFails[1], allFails[2], allFails[3])); 47 | const result = parser3.parse("a") as ParjsFailure; 48 | expect(result.reason).toBe(reasons.join(" OR ")); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/flatten.spec.ts: -------------------------------------------------------------------------------- 1 | import { string } from "@lib"; 2 | import { each, flatten, map, mapConst } from "@lib/combinators"; 3 | 4 | describe("flatten", () => { 5 | const q = string("a"); 6 | it("works with non-array item", () => { 7 | const p = q.pipe( 8 | flatten(), 9 | each(arr => { 10 | arr.map(x => x.toUpperCase()); 11 | }) 12 | ); 13 | expect(p.parse("a")).toBeSuccessful(["a"]); 14 | }); 15 | 16 | it("works with level-1 array", () => { 17 | const p = q.pipe( 18 | map(() => ["a", "b"]), 19 | flatten(), 20 | each(arr => { 21 | arr.map(x => x.toUpperCase()); 22 | }) 23 | ); 24 | expect(p.parse("a")).toBeSuccessful(["a", "b"]); 25 | }); 26 | 27 | it("works with level-2 array with nesting", () => { 28 | const p = q.pipe( 29 | map(() => [["a"], "b", [], ["c", "d", "e"]] as string[]), 30 | flatten(), 31 | each(arr => { 32 | arr[0].slice(); 33 | arr.map(x => x.toUpperCase()); 34 | }) 35 | ); 36 | expect(p.parse("a")).toBeSuccessful(["a", "b", "c", "d", "e"]); 37 | }); 38 | 39 | it("works with level-2 array with nesting, take 2", () => { 40 | const p = q.pipe( 41 | mapConst(["a", [], ["b", ["c"]], ["d"], [[]]]), 42 | map(x => x as string[]), 43 | flatten(), 44 | each(arr => { 45 | arr.map(x => x.toUpperCase()); 46 | }) 47 | ); 48 | expect(p.parse("a")).toBeSuccessful(["a", "b", "c", "d"]); 49 | }); 50 | 51 | it("worked with level-3 array with nesting", () => { 52 | const p = q.pipe( 53 | mapConst(["a", ["b", ["c", ["d"]]], [[]], [[], "e"], [[["f"], "g"]], "h"] as string[]), 54 | flatten(), 55 | each(x => { 56 | x.forEach(a => a.toUpperCase()); 57 | }) 58 | ); 59 | 60 | expect(p.parse("a")).toBeSuccessful(["a", "b", "c", "d", "e", "f", "g", "h"]); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /.github/workflows/char-info.push.yaml: -------------------------------------------------------------------------------- 1 | name: publish char-info 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - "packages/char-info/**" 8 | - ".github/workflows/char-info.push.yaml" 9 | - "yaml.lock" 10 | - ".*" 11 | workflow_dispatch: 12 | 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: false 21 | 22 | jobs: 23 | build_deploy: 24 | environment: 25 | name: github-pages 26 | url: ${{ steps.deployment.outputs.page_url }} 27 | runs-on: ubuntu-latest 28 | strategy: 29 | matrix: 30 | node-version: 31 | - 22.13.0 32 | env: 33 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | steps: 35 | - name: CHECKOUT CODE 36 | uses: actions/checkout@v4 37 | 38 | - name: SETUP COREPACK 39 | run: corepack enable 40 | 41 | - name: SETUP NODE.JS ${{ matrix.node-version }} 42 | uses: actions/setup-node@v3 43 | with: 44 | node-version: "22.13.0" 45 | cache: yarn 46 | registry-url: "https://registry.yarnpkg.com" 47 | 48 | - name: SETUP YARN 49 | run: yarn set version stable 50 | - name: INSTALL 51 | run: yarn install --immutable 52 | 53 | - name: BUILD 54 | run: yarn workspaces foreach -Rpt --from '{char-info,char-info}' run build 55 | 56 | - name: LINT 57 | run: yarn workspace char-info run lint:check 58 | 59 | - name: TEST 60 | run: yarn workspace char-info run test:coverage 61 | 62 | - name: Upload coverage reports to Codecov with GitHub Action 63 | uses: codecov/codecov-action@v4.2.0 64 | env: 65 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 66 | with: 67 | directory: packages/char-info/coverage 68 | flags: char-info 69 | 70 | - name: SETUP PUBLISH TOKEN 71 | run: 'echo "npmAuthToken: ${NPM_TOKEN}" >> ~/.yarnrc.yml' 72 | 73 | - name: PUBLISH CHAR-INFO PACKAGE 74 | working-directory: packages/char-info 75 | run: yarn npm publish --access public 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | .obsidian/ 6 | docs/ 7 | .DS_STORE 8 | .idea/ 9 | .tmp/ 10 | # User-specific stuff: 11 | .idea/workspace.xml 12 | .idea/tasks.xml 13 | .idea/dictionaries 14 | .idea/vcs.xml 15 | .idea/jsLibraryMappings.xml 16 | 17 | # Sensitive or high-churn files: 18 | .idea/dataSources.ids 19 | .idea/dataSources.xml 20 | .idea/dataSources.local.xml 21 | .idea/sqlDataSources.xml 22 | .idea/dynamic.xml 23 | .idea/uiDesigner.xml 24 | 25 | # Gradle: 26 | .idea/gradle.xml 27 | .idea/libraries 28 | 29 | # Mongo Explorer plugin: 30 | .idea/mongoSettings.xml 31 | 32 | ## File-based project format: 33 | *.iws 34 | 35 | ## Plugin-specific files: 36 | 37 | # IntelliJ 38 | /out/ 39 | 40 | # mpeltonen/sbt-idea plugin 41 | .idea_modules/ 42 | 43 | # JIRA plugin 44 | atlassian-ide-plugin.xml 45 | 46 | # Crashlytics plugin (for Android Studio and IntelliJ) 47 | com_crashlytics_export_strings.xml 48 | crashlytics.properties 49 | crashlytics-build.properties 50 | fabric.properties 51 | ### Node template 52 | # Logs 53 | logs 54 | *.log 55 | npm-debug.log* 56 | 57 | # Runtime data 58 | pids 59 | *.pid 60 | *.seed 61 | 62 | # Directory for instrumented libs generated by jscoverage/JSCover 63 | lib-cov 64 | dist/ 65 | # Coverage directory used by tools like istanbul 66 | coverage 67 | 68 | # nyc test coverage 69 | .nyc_output 70 | 71 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 72 | .grunt 73 | 74 | # node-waf configuration 75 | .lock-wscript 76 | 77 | # Compiled binary addons (http://nodejs.org/api/addons.html) 78 | build/Release 79 | 80 | # Dependency directories 81 | node_modules 82 | jspm_packages 83 | 84 | # Optional npm cache directory 85 | .npm 86 | 87 | # Optional REPL history 88 | .node_repl_history 89 | 90 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 91 | .pnp.* 92 | .yarn/* 93 | !.yarn/patches 94 | !.yarn/plugins 95 | !.yarn/releases 96 | !.yarn/sdks 97 | !.yarn/versions 98 | **/dist-spec 99 | *.cpuprofile -------------------------------------------------------------------------------- /packages/char-info/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "char-info", 3 | "version": "0.3.6", 4 | "description": "Unicode character information library.", 5 | "keywords": [ 6 | "text", 7 | "string", 8 | "character", 9 | "char", 10 | "indicator", 11 | "isUpper", 12 | "isLower", 13 | "unicode", 14 | "utf8", 15 | "utf16" 16 | ], 17 | "homepage": "https://char-info.parjs.org", 18 | "repository": "https://github.com/GregRos/char-info", 19 | "license": "MIT", 20 | "author": "GregRos ", 21 | "sideEffects": false, 22 | "exports": { 23 | ".": { 24 | "require": "./dist/index.js", 25 | "import": "./dist/index.js", 26 | "default": "./dist/index.js", 27 | "types": "./dist/index.d.ts" 28 | }, 29 | "./ascii": { 30 | "require": "./dist/ascii.js", 31 | "import": "./dist/ascii.js", 32 | "default": "./dist/ascii.js", 33 | "types": "./dist/ascii.d.ts" 34 | } 35 | }, 36 | "main": "dist/index.js", 37 | "typesVersions": { 38 | "*": { 39 | ".": [ 40 | "./dist/index.d.ts" 41 | ], 42 | "ascii": [ 43 | "./dist/ascii.d.ts" 44 | ] 45 | } 46 | }, 47 | "typings": "dist/index", 48 | "files": [ 49 | "dist", 50 | "src", 51 | "README.md", 52 | "LICENSE.md", 53 | "package.json", 54 | "tsconfig.json" 55 | ], 56 | "scripts": { 57 | "build": "tsc -b .", 58 | "build:clean": "run-s clean build", 59 | "clean": "shx rm -rf dist dist-spec", 60 | "test": "../../node_modules/ava/cli.js dist-spec/**/*.spec.js --verbose", 61 | "test:coverage": "nyc ../../node_modules/ava/cli.js dist-spec/**/*.spec.js --verbose" 62 | }, 63 | "ava": { 64 | "files": [ 65 | "dist-spec/**/*.spec.js" 66 | ] 67 | }, 68 | "nyc": { 69 | "all": true, 70 | "include": [ 71 | "dist/**/*.js", 72 | "src/**/*.ts" 73 | ], 74 | "reporter": [ 75 | "lcov", 76 | "text-summary" 77 | ], 78 | "sourceMap": true 79 | }, 80 | "dependencies": { 81 | "node-interval-tree": "^1.3.3" 82 | }, 83 | "devDependencies": { 84 | "ava": "^3", 85 | "npm-run-all": "^4.1.5", 86 | "nyc": "^15.1.0", 87 | "typescript": "^5.4.5" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/many.spec.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser } from "@lib"; 2 | import { ResultKind, eof, fail, result, string } from "@lib"; 3 | import { many, thenq } from "@lib/combinators"; 4 | import { range } from "@lib/utils"; 5 | 6 | describe("many combinators", () => { 7 | describe("regular many", () => { 8 | const parser: Parjser<"ab"[]> = string("ab").pipe(many()); 9 | it("success on empty input", () => { 10 | expect(parser.parse("")).toBeSuccessful([]); 11 | }); 12 | it("failure on non-empty input without any matches", () => { 13 | expect(parser.parse("12")).toBeFailure(ResultKind.SoftFail); 14 | }); 15 | it("success on single match", () => { 16 | expect(parser.parse("ab")).toBeSuccessful(["ab"]); 17 | }); 18 | it("success on N matches", () => { 19 | expect(parser.parse("ababab")).toBeSuccessful(["ab", "ab", "ab"]); 20 | }); 21 | it("chains to EOF correctly", () => { 22 | const endEof = parser.pipe(thenq(eof())); 23 | expect(endEof.parse("abab")).toBeSuccessful(["ab", "ab"]); 24 | }); 25 | it("fails hard when many fails hard", () => { 26 | const parser2 = fail().pipe(many()); 27 | expect(parser2.parse("")).toBeFailure("Hard"); 28 | }); 29 | }); 30 | 31 | describe("many with zero-length match", () => { 32 | it("guards against zero match in inner parser", () => { 33 | const parser = result(0).pipe(many()); 34 | expect(() => parser.parse("")).toThrow(); 35 | }); 36 | 37 | it("ignores guard when given max iterations", () => { 38 | const parser = result(0).pipe(many(10)); 39 | expect(parser.parse("")).toBeSuccessful(range(0, 10).map(() => 0)); 40 | }); 41 | }); 42 | 43 | describe("many with bounded iterations, min successes", () => { 44 | const parser = string("ab").pipe(many(2)); 45 | it("succeeds when appropriate", () => { 46 | expect(parser.parse("abab")).toBeSuccessful(["ab", "ab"]); 47 | }); 48 | it("fails when there is excess input", () => { 49 | expect(parser.parse("ababab")).toBeFailure(ResultKind.SoftFail); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "license": "MIT", 4 | "workspaces": [ 5 | "packages/*", 6 | "packages/*/examples" 7 | ], 8 | "scripts": { 9 | "build": "tsc -b", 10 | "watch": "tsc -b -w", 11 | "test": "yarn workspaces foreach -Rpt --from '{char-info,parjs}' run test", 12 | "clean": "yarn workspaces foreach -A run clean", 13 | "build:clean": "run-s clean build", 14 | "prettier:check": "prettier --check .", 15 | "prettier:fix": "prettier --write .", 16 | "eslint:check": "eslint", 17 | "eslint:fix": "eslint --fix", 18 | "lint:check": "run-s eslint:check prettier:check", 19 | "lint:fix": "run-s eslint:fix prettier:fix", 20 | "test:coverage": "yarn workspaces foreach -Rpt --from '{char-info,parjs}' run test:coverage", 21 | "tidy": "yarn install && run-s lint:fix build:clean test" 22 | }, 23 | "husky": { 24 | "hooks": { 25 | "pre-commit": "yarn run build:clean && lint-staged" 26 | } 27 | }, 28 | "lint-staged": { 29 | "*.{ts,js,mjs,mts,cjs,cts}": "eslint --cache --fix", 30 | "*.{ts,js,mts,mjs,cts,cjs,css,md,yml,yaml,json}": "prettier --write" 31 | }, 32 | "prettier": "./.prettierrc.json", 33 | "dependencies": { 34 | "char-info": "workspace:^", 35 | "parjs": "workspace:^" 36 | }, 37 | "devDependencies": { 38 | "@eslint/js": "^9.32.0", 39 | "@stylistic/eslint-plugin": "^5.1.0", 40 | "@types/node": "^22", 41 | "@typescript-eslint/eslint-plugin": "^8.39.0", 42 | "@typescript-eslint/parser": "^8.39.0", 43 | "@typescript-eslint/utils": "^8.36.0", 44 | "eslint": "^9.32.0", 45 | "eslint-plugin-jest": "^29.0.1", 46 | "eslint-plugin-markdown": "^5.1.0", 47 | "husky": "9.0.11", 48 | "lint-staged": "15.2.2", 49 | "npm-run-all": "^4.1.5", 50 | "prettier": "^3.3.1", 51 | "prettier-plugin-jsdoc": "^1.3.0", 52 | "prettier-plugin-organize-imports": "3.2.4", 53 | "prettier-plugin-packagejson": "2.5.0", 54 | "shelljs": "0.8.5", 55 | "shx": "0.3.4", 56 | "ts-node": "10.9.1", 57 | "typedoc": "^0.28.5", 58 | "typedoc-material-theme": "^1.4.0", 59 | "typedoc-plugin-extras": "^4.0.1", 60 | "typedoc-plugin-mdn-links": "^5.0.6", 61 | "typescript": "^5.4.5" 62 | }, 63 | "packageManager": "yarn@4.6.0", 64 | "engines": { 65 | "node": ">=16.0.0" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/parsers/int.ts: -------------------------------------------------------------------------------- 1 | import { ParserDefinitionError } from "../../errors"; 2 | import { defaults } from "../../utils"; 3 | import type { Parjser } from "../parjser"; 4 | import { ParjserBase } from "../parser"; 5 | import { ResultKind } from "../result"; 6 | import type { ParsingState } from "../state"; 7 | import { NumericHelpers } from "./numeric-helpers"; 8 | 9 | /** A set of options for parsing integers. */ 10 | export interface IntOptions { 11 | allowSign: boolean; 12 | base: number; 13 | } 14 | 15 | const defaultOptions: IntOptions = { 16 | allowSign: true, 17 | base: 10 18 | }; 19 | 20 | class Int extends ParjserBase { 21 | type = "int"; 22 | 23 | constructor( 24 | readonly options: IntOptions, 25 | readonly expecting: string 26 | ) { 27 | super(); 28 | } 29 | 30 | _apply(ps: ParsingState): void { 31 | const { allowSign, base } = this.options; 32 | let { position } = ps; 33 | const { input } = ps; 34 | const initPos = ps.position; 35 | let sign = allowSign ? NumericHelpers.parseSign(ps) : 0; 36 | let parsedSign = false; 37 | if (sign !== 0) { 38 | parsedSign = true; 39 | } else { 40 | sign = 1; 41 | } 42 | position = ps.position; 43 | NumericHelpers.parseDigitsInBase(ps, base); 44 | const value = parseInt(input.substring(initPos, ps.position), base); 45 | 46 | if (ps.position === position) { 47 | ps.kind = parsedSign ? ResultKind.HardFail : ResultKind.SoftFail; 48 | } else { 49 | ps.value = value; 50 | ps.kind = ResultKind.Ok; 51 | } 52 | } 53 | } 54 | 55 | /** 56 | * Returns a parser that will parse a single integer, with the options given by `options`. 57 | * 58 | * @param pOptions A set of options for parsing integers. 59 | */ 60 | export function int(pOptions?: Partial): Parjser { 61 | const options = defaults(pOptions, defaultOptions); 62 | if (options.base > 36) { 63 | throw new ParserDefinitionError("int", "invalid base"); 64 | } 65 | const expecting = `expecting a ${options.allowSign ? "signed" : "unsigned"} integer in base ${ 66 | options.base 67 | }`; 68 | return new Int(options, expecting); 69 | } 70 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/reason.ts: -------------------------------------------------------------------------------- 1 | import type { CombinatorInput } from "../combinated"; 2 | import { Combinated } from "../combinated"; 3 | import type { ParjsCombinator } from "../parjser"; 4 | import type { FailureInfo } from "../result"; 5 | import type { ParsingState } from "../state"; 6 | import type { ImplicitParjser } from "../wrap-implicit"; 7 | import { wrapImplicit } from "../wrap-implicit"; 8 | 9 | class Expects extends Combinated { 10 | type = "expects"; 11 | 12 | expecting = typeof this.messageOrFunction === "string" ? this.messageOrFunction : ""; 13 | constructor( 14 | source: CombinatorInput, 15 | private readonly messageOrFunction: string | ((failure: FailureInfo) => string) 16 | ) { 17 | super(source); 18 | } 19 | _apply(ps: ParsingState): void { 20 | const { position } = ps; 21 | const { messageOrFunction } = this; 22 | this.source.apply(ps); 23 | if (ps.kind !== "OK") { 24 | ps.reason = 25 | typeof messageOrFunction === "string" 26 | ? messageOrFunction 27 | : messageOrFunction({ 28 | kind: ps.kind, 29 | reason: ps.reason! // the error is guaranteed to be non-null 30 | }); 31 | return; 32 | } 33 | ps.position = position; 34 | } 35 | } 36 | 37 | /** 38 | * Applies the source parser, modifying the failure reason to the given message if it fails Softly. 39 | * Useful for providing the user with a more meaningful error message than was is provided by 40 | * default. 41 | * 42 | * @param message The new message. 43 | */ 44 | export function reason(message: string): ParjsCombinator; 45 | /** 46 | * Applies the source parser. If it fails, calls `onFailure` with the failure info and uses the 47 | * returned string as the new failure reason. 48 | * 49 | * @param onFailure A function that takes the failure info and returns the new reason. 50 | */ 51 | export function reason(onFailure: (failure: FailureInfo) => string): ParjsCombinator; 52 | export function reason(messageOrFunction: string | ((failure: FailureInfo) => string)) { 53 | return (source: ImplicitParjser) => new Expects(wrapImplicit(source), messageOrFunction); 54 | } 55 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/recover.ts: -------------------------------------------------------------------------------- 1 | import type { ParjsCombinator } from "../../"; 2 | import type { CombinatorInput } from "../combinated"; 3 | import { Combinated } from "../combinated"; 4 | import type { FailureInfo, ResultKindFail, SuccessInfo } from "../result"; 5 | import type { ParsingState, UserState } from "../state"; 6 | import { wrapImplicit } from "../wrap-implicit"; 7 | 8 | /** Information about the failure. */ 9 | export interface ParserFailureState { 10 | /** The parser's user state at the moment of failure. */ 11 | readonly userState: UserState; 12 | /** The reason reported by the failure. */ 13 | readonly reason: string | object; 14 | /** The severity of the failure. */ 15 | readonly kind: ResultKindFail; 16 | } 17 | 18 | /** 19 | * Function used to recover from a failure. The `kind`, `reason`, and `value` fields of the result 20 | * are used to determine the new parser success state. You can return a falsy value to indicate 21 | * nothing should change. 22 | */ 23 | export type RecoveryFunction = ( 24 | failure: ParserFailureState 25 | ) => SuccessInfo | Partial | null; 26 | 27 | class Soft extends Combinated { 28 | type = "recover"; 29 | expecting = this.source.expecting; 30 | 31 | constructor( 32 | source: CombinatorInput, 33 | private readonly recoverFunction: RecoveryFunction 34 | ) { 35 | super(source); 36 | } 37 | 38 | _apply(ps: ParsingState): void { 39 | this.source.apply(ps); 40 | if (ps.isOk || ps.isFatal) return; 41 | const result = this.recoverFunction({ 42 | userState: ps.userState, 43 | kind: ps.kind as ResultKindFail, 44 | reason: ps.reason! // the error is guaranteed to be non-null 45 | } satisfies ParserFailureState); 46 | if (!result) return; 47 | ps.kind = result.kind || ps.kind; 48 | if (result.kind === "OK") { 49 | ps.value = result.value; 50 | } else { 51 | ps.reason = result.reason || ps.reason; 52 | } 53 | } 54 | } 55 | 56 | /** Reduces Hard failures to Soft ones and behaves in the same way on success. */ 57 | export function recover(recoverFunction: RecoveryFunction): ParjsCombinator { 58 | return source => new Soft(wrapImplicit(source), recoverFunction); 59 | } 60 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/or.ts: -------------------------------------------------------------------------------- 1 | import type { CombinatorInput } from "../combinated"; 2 | import { Combinated } from "../combinated"; 3 | import type { ParjsCombinator } from "../parjser"; 4 | import { ResultKind } from "../result"; 5 | import type { ParsingState } from "../state"; 6 | import type { ImplicitParjser } from "../wrap-implicit"; 7 | import { wrapImplicit } from "../wrap-implicit"; 8 | 9 | import type { getParsedType } from "../util-types"; 10 | 11 | class Or[]> extends Combinated< 12 | T, 13 | T | getParsedType 14 | > { 15 | type = "or"; 16 | _altNames = [this.source, ...this._alts].map(x => `'${x.expecting}' (${x.type})`); 17 | expecting = `expecting one of: ${this._altNames.join(", ")}`; 18 | 19 | constructor( 20 | source: CombinatorInput, 21 | private readonly _alts: Alts 22 | ) { 23 | super(source); 24 | } 25 | 26 | _apply(ps: ParsingState): void { 27 | const { position } = ps; 28 | const resolvedAlts = [this.source, ...this._alts]; 29 | const allExpectations = resolvedAlts.map(x => x.expecting); 30 | for (let i = 0; i < resolvedAlts.length; i++) { 31 | // go over each alternative. 32 | const cur = resolvedAlts[i]; 33 | // apply it on the current state. 34 | cur.apply(ps); 35 | if (ps.isOk) { 36 | // if success, return. The PS records the result. 37 | return; 38 | } else if (ps.isSoft) { 39 | // backtrack to the original position and try again. 40 | ps.position = position; 41 | allExpectations[i] = ps.reason!; 42 | } else { 43 | // propagate hard failure 44 | return; 45 | } 46 | } 47 | ps.reason = allExpectations.join(" OR "); 48 | ps.kind = ResultKind.SoftFail; 49 | } 50 | } 51 | 52 | export function or[]>( 53 | ...alts: Opts 54 | ): ParjsCombinator> { 55 | const resolvedAlts = alts.map(wrapImplicit) as { 56 | [K in keyof Opts]: CombinatorInput>; 57 | }; 58 | return (source: ImplicitParjser) => { 59 | return new Or(wrapImplicit(source), resolvedAlts); 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/manySepBy.spec.ts: -------------------------------------------------------------------------------- 1 | import { ResultKind, fail, result, string } from "@lib"; 2 | import { manySepBy, stringify, then, thenq } from "@lib/combinators"; 3 | import { getArrayWithSeparators } from "@lib/internal/combinators/many-sep-by"; 4 | 5 | const prs = string("ab"); 6 | 7 | describe("manySepBy combinator", () => { 8 | const parser = prs.pipe(manySepBy(", ")); 9 | 10 | it("works with max iterations", () => { 11 | const parser2 = prs.pipe(manySepBy(", ", 2)); 12 | const parser3 = parser2.pipe(thenq(string(", ab"))); 13 | expect(parser3.parse("ab, ab, ab")).toBeSuccessful(); 14 | }); 15 | 16 | it("succeeds with empty input", () => { 17 | expect(parser.parse("")).toBeSuccessful(getArrayWithSeparators([], [])); 18 | }); 19 | 20 | it("many fails hard on 1st application", () => { 21 | const parser2 = fail().pipe(manySepBy(result(""))); 22 | expect(parser2.parse("")).toBeFailure("Hard"); 23 | }); 24 | 25 | it("sep fails hard", () => { 26 | const parser2 = prs.pipe(manySepBy(fail())); 27 | expect(parser2.parse("ab, ab")).toBeFailure("Hard"); 28 | }); 29 | 30 | it("sep+many that don't consume throw without max iterations", () => { 31 | const parser2 = string("").pipe(manySepBy("")); 32 | expect(() => parser2.parse("")).toThrow(); 33 | }); 34 | 35 | it("sep+many that don't consume succeed with max iterations", () => { 36 | const parser2 = string("").pipe(manySepBy("", 2)); 37 | expect(parser2.parse("")).toBeSuccessful(getArrayWithSeparators(["", ""], [""])); 38 | }); 39 | 40 | it("many that fails hard on 2nd iteration", () => { 41 | const manyParser = string("a").pipe(then("b"), stringify(), manySepBy(", ")); 42 | expect(manyParser.parse("ab, ac")).toBeFailure("Hard"); 43 | }); 44 | 45 | it("succeeds with non-empty input", () => { 46 | expect(parser.parse("ab, ab")).toBeSuccessful(getArrayWithSeparators(["ab", "ab"], [", "])); 47 | }); 48 | 49 | it("chains into terminating separator", () => { 50 | const parser2 = parser.pipe(thenq(", ")); 51 | expect(parser2.parse("ab, ab, ")).toBeSuccessful( 52 | getArrayWithSeparators(["ab", "ab"], [", ", ", "]) 53 | ); 54 | }); 55 | it("fails soft if first many fails", () => { 56 | expect(parser.parse("xa")).toBeFailure(ResultKind.SoftFail); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /parjs.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "./packages/parjs", 5 | "name": "parjs" 6 | }, 7 | { 8 | "path": "./packages/char-info", 9 | "name": "char-info" 10 | }, 11 | { 12 | "path": ".", 13 | "name": "root" 14 | } 15 | ], 16 | "settings": { 17 | "eslint.lintTask.enable": true, 18 | "jest.disabledWorkspaceFolders": ["root"], 19 | "files.exclude": { 20 | "packages/parjs": true, 21 | "packages/char-info": true, 22 | "**/.git": true, 23 | "**/.svn": true, 24 | "**/.hg": true, 25 | "**/.nyc_output": true, 26 | "**/coverage": true, 27 | "**/dist-spec": true, 28 | "**/CVS": true, 29 | "**/.DS_Store": true, 30 | "**/Thumbs.db": true, 31 | "**/*.aux": true, 32 | "**/*.bbl": true, 33 | "**/*.fdb_latexmk": true, 34 | "**/*.fls": true, 35 | "**/*.out": true, 36 | "**/*.synctex.gz": true, 37 | "**/.idea": true, 38 | "**/.mypy_cache": true, 39 | "**/__pycache__": true, 40 | "**/dist": true, 41 | "**/dist_aux": true, 42 | ".nyc_output": true, 43 | "coverage": true, 44 | "docs": true 45 | }, 46 | "typescript.tsserver.experimental.enableProjectDiagnostics": true, 47 | // GregRos @ 2024 48 | // For this to work, you need the 'APC Customize UI++' extension. 49 | // It will color the rows in the explorer view based on the workspace folder. 50 | // Even if you don't use the extension, please don't remove it, unless you don't know who I am. 51 | "apc.stylesheet": { 52 | ".explorer-folders-view.explorer-folders-view .monaco-list-row:has(.monaco-icon-label[aria-label*='packages\\\\parjs'])": { 53 | "background-image": "linear-gradient(to right, #0E5DE610, transparent)", 54 | "border-left": "2px solid #7DADFF7D" 55 | }, 56 | ".explorer-folders-view.explorer-folders-view .monaco-list-row:has(.monaco-icon-label[aria-label*='packages\\\\char-info'])": { 57 | "background-image": "linear-gradient(to right, #6EED1314, transparent)", 58 | "border-left": "2px solid #6EED137C" 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/parjs/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function cloneDeep(source: T): T { 2 | if (typeof source !== "object" || !source) { 3 | return source; 4 | } 5 | if (Array.isArray(source)) { 6 | return source.map(x => cloneDeep(x)) as never; 7 | } 8 | const newObj = Object.create(Object.getPrototypeOf(source)); 9 | for (const key of Object.keys(source)) { 10 | newObj[key] = cloneDeep((source as never)[key]); 11 | } 12 | return newObj; 13 | } 14 | 15 | // Based on lodash's implementation: https://github.com/lodash/lodash 16 | function _defaultsDeep(target: any, source: any) { 17 | target = Object(target); 18 | if (!source) return target; 19 | source = Object(source); 20 | for (const key of Object.keys(source)) { 21 | const value = source[key]; 22 | if (typeof target[key] === "object") { 23 | defaultsDeep(target[key], Object(value)); 24 | } else { 25 | if (target[key] === undefined) { 26 | target[key] = value; 27 | } 28 | } 29 | } 30 | return target; 31 | } 32 | 33 | export function defaultsDeep(target: A, ...sources: B[]): A & B { 34 | for (const source of sources) { 35 | target = _defaultsDeep(target, source); 36 | } 37 | return target as A & B; 38 | } 39 | 40 | export function defaults>( 41 | arg: T | undefined, 42 | ...sources: (S | undefined)[] 43 | ): T & S { 44 | const target = Object(arg); 45 | for (const source of sources) { 46 | if (!source) { 47 | continue; 48 | } 49 | for (const key of Object.keys(source) as (keyof T)[]) { 50 | if (!(key in target) || target[key] === undefined) { 51 | target[key] = source[key] as any; 52 | } 53 | } 54 | } 55 | return target as T & S; 56 | } 57 | 58 | export function clone(source: T): T { 59 | if (typeof source !== "object" || !source) { 60 | return source; 61 | } 62 | if (Array.isArray(source)) { 63 | return source.slice() as never; 64 | } 65 | const newObj = Object.create(Object.getPrototypeOf(source)); 66 | for (const key of Object.keys(source) as (keyof T)[]) { 67 | newObj[key] = source[key]; 68 | } 69 | return newObj; 70 | } 71 | 72 | export function range(start: number, end: number): number[] { 73 | const arr = []; 74 | for (let i = start; i < end; i++) { 75 | arr.push(i); 76 | } 77 | return arr; 78 | } 79 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/trace-visualizer.ts: -------------------------------------------------------------------------------- 1 | /** @module parjs/trace */ 2 | import type { Trace } from "./result"; 3 | 4 | import { defaults } from "../utils"; 5 | import { padInt } from "./functions"; 6 | import type { ParjserBase } from "./parser"; 7 | 8 | /** A set of arguments for the trace visualizer. */ 9 | export interface TraceVisualizerArgs { 10 | lineNumbers: boolean; 11 | linesBefore: number; 12 | } 13 | 14 | /** A function that prints out a nice visualization of where a parser failed. */ 15 | export interface TraceVisualizer { 16 | (trace: Trace): string; 17 | configure(args: Partial): TraceVisualizer; 18 | } 19 | 20 | const defaultArgs: TraceVisualizerArgs = { 21 | lineNumbers: true, 22 | linesBefore: 1 23 | }; 24 | 25 | function newTraceVisualizer(pAgs: Partial) { 26 | const args = defaults(pAgs, defaultArgs); 27 | const visualizer: TraceVisualizer = (trace: Trace) => { 28 | const rows = trace.input.split(/\r\n|\n|\r/g); 29 | const locRow = trace.location.line; 30 | const around = args.linesBefore; 31 | const firstRow = Math.max(0, locRow - around); 32 | let linesAround = rows.slice(firstRow, locRow + 1); 33 | 34 | let prefixLength = 0; 35 | if (args.lineNumbers) { 36 | const numLength = Math.floor(1 + Math.log(locRow + 1) / Math.log(10)); 37 | const rowNumberPrefixer = (n: number) => `${padInt(firstRow + n, numLength, " ")} | `; 38 | prefixLength = numLength + 3; 39 | linesAround = linesAround.map((row, i) => `${rowNumberPrefixer(i + 1)}${row}`); 40 | } 41 | const errorMarked = `${" ".repeat(prefixLength + trace.location.column)}^${trace.reason}`; 42 | linesAround.push(errorMarked); 43 | const linesVisualization = linesAround.join("\n"); 44 | 45 | const stack = trace.stackTrace 46 | .map(x => { 47 | const base = x as ParjserBase; 48 | return `${base.expecting} (${x.type})`; 49 | }) 50 | .filter(x => x) 51 | .join("\n"); 52 | const fullVisualization = `${trace.kind} failure at Ln ${trace.location.line + 1} Col ${ 53 | trace.location.column + 1 54 | } 55 | ${linesVisualization} 56 | 57 | Stack: 58 | ${stack} 59 | `; 60 | return fullVisualization; 61 | }; 62 | visualizer.configure = newTraceVisualizer; 63 | return visualizer; 64 | } 65 | 66 | /** Visualizes a Parjs failure. */ 67 | export const visualizeTrace = newTraceVisualizer(defaultArgs); 68 | -------------------------------------------------------------------------------- /packages/parjs/examples/spec/json.spec.ts: -------------------------------------------------------------------------------- 1 | import { exampleInput, pJsonValue } from "../src/json"; 2 | 3 | describe("the JSON example", () => { 4 | describe("a complex example", () => { 5 | const successInput = `{"a" : 2, 6 | 7 | 8 | "b\\"" : 9 | 44325, "z" : "hi!", "a" : true, 10 | "array" : ["hi", 1, {"a" : "b\\"" }, [], {}]}`; 11 | 12 | const parser = pJsonValue; 13 | 14 | it("can parse a complex example", () => { 15 | const result = parser.parse(successInput); 16 | expect(result).toBeSuccessful(); 17 | }); 18 | }); 19 | 20 | it("can parse the example input", () => { 21 | const result = pJsonValue.parse(exampleInput); 22 | expect(result).toMatchInlineSnapshot(` 23 | ParjsSuccess { 24 | "kind": "OK", 25 | "value": JsonObject { 26 | "value": [ 27 | JsonObjectProperty { 28 | "name": "a", 29 | "value": JsonNumber { 30 | "value": 2, 31 | }, 32 | }, 33 | JsonObjectProperty { 34 | "name": "b"", 35 | "value": JsonNumber { 36 | "value": 44325, 37 | }, 38 | }, 39 | JsonObjectProperty { 40 | "name": "z", 41 | "value": JsonString { 42 | "value": "hi!", 43 | }, 44 | }, 45 | JsonObjectProperty { 46 | "name": "a", 47 | "value": JsonBool { 48 | "value": true, 49 | }, 50 | }, 51 | JsonObjectProperty { 52 | "name": "array", 53 | "value": JsonArray { 54 | "value": [ 55 | JsonString { 56 | "value": "hi", 57 | }, 58 | JsonNumber { 59 | "value": 1, 60 | }, 61 | JsonObject { 62 | "value": [ 63 | JsonObjectProperty { 64 | "name": "a", 65 | "value": JsonString { 66 | "value": "b"", 67 | }, 68 | }, 69 | ], 70 | }, 71 | JsonArray { 72 | "value": [], 73 | }, 74 | JsonObject { 75 | "value": [], 76 | }, 77 | ], 78 | }, 79 | }, 80 | ], 81 | }, 82 | } 83 | `); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/mappers.spec.ts: -------------------------------------------------------------------------------- 1 | import { anyCharOf, eof, result, ResultKind, string, stringLen } from "@lib"; 2 | import { each, map, stringify } from "@lib/combinators"; 3 | 4 | const goodInput = "abcd"; 5 | const badInput = ""; 6 | const uState = {}; 7 | const loudParser = stringLen(4); 8 | 9 | describe("map combinators", () => { 10 | describe("map", () => { 11 | const parser = loudParser.pipe(map(() => 1)); 12 | it("maps on success", () => { 13 | expect(parser.parse(goodInput, uState)).toBeSuccessful(1); 14 | }); 15 | it("fails on failure", () => { 16 | expect(parser.parse(badInput, uState)).toBeFailure(ResultKind.SoftFail); 17 | }); 18 | }); 19 | 20 | describe("cast", () => { 21 | const parser = loudParser.pipe(map(x => x as unknown as number)); 22 | it("maps on success", () => { 23 | expect(parser.parse(goodInput)).toBeSuccessful("abcd" as never); 24 | }); 25 | it("fails on failure", () => { 26 | expect(parser.parse(badInput)).toBeFailure(); 27 | }); 28 | }); 29 | 30 | describe("stringify", () => { 31 | it("quiet", () => { 32 | const p = eof().pipe(map(() => "")); 33 | expect(p.parse("")).toBeSuccessful(""); 34 | }); 35 | 36 | it("array", () => { 37 | const p = result(["a", "b", "c"]).pipe(stringify()); 38 | expect(p.parse("")).toBeSuccessful("abc"); 39 | }); 40 | 41 | it("nested array", () => { 42 | const p = result(["a", ["b", ["c"], "d"], "e"]).pipe(stringify()); 43 | expect(p.parse("")).toBeSuccessful("abcde"); 44 | }); 45 | 46 | it("null", () => { 47 | const p = result(null).pipe(stringify()); 48 | expect(p.parse("")).toBeSuccessful("null"); 49 | }); 50 | 51 | it("undefined", () => { 52 | const p = result(undefined).pipe(stringify()); 53 | expect(p.parse("")).toBeSuccessful("undefined"); 54 | }); 55 | 56 | it("string", () => { 57 | const p = string("a").pipe(stringify()); 58 | expect(p.parse("a")).toBeSuccessful("a"); 59 | }); 60 | 61 | it("object", () => { 62 | const p = result({}).pipe(stringify()); 63 | expect(p.parse("")).toBeSuccessful({}.toString()); 64 | }); 65 | }); 66 | 67 | describe("each", () => { 68 | let tally = ""; 69 | const p = anyCharOf("abc").pipe( 70 | each((res, state) => { 71 | tally += res; 72 | state.char = res; 73 | }) 74 | ); 75 | it("works", () => { 76 | expect(p.parse("a")).toBeSuccessful("a"); 77 | expect(tally).toBe("a"); 78 | expect(p.parse("b")).toBeSuccessful("b"); 79 | expect(tally).toBe("ab"); 80 | expect(p.parse("d")).toBeFailure("Soft"); 81 | expect(tally).toBe("ab"); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /packages/char-info/src/names/scripts.ts: -------------------------------------------------------------------------------- 1 | /** @module char-info/unicode */ 2 | /** Unicode script names. */ 3 | export namespace UnicodeScript { 4 | export const Arabic = "Arabic"; 5 | export const Armenian = "Armenian"; 6 | export const Balinese = "Balinese"; 7 | export const Bamum = "Bamum"; 8 | export const Batak = "Batak"; 9 | export const Bengali = "Bengali"; 10 | export const Bopomofo = "Bopomofo"; 11 | export const Braille = "Braille"; 12 | export const Buginese = "Buginese"; 13 | export const Buhid = "Buhid"; 14 | export const CanadianAboriginal = "CanadianAboriginal"; 15 | export const Cham = "Cham"; 16 | export const Cherokee = "Cherokee"; 17 | export const Common = "Common"; 18 | export const Coptic = "Coptic"; 19 | export const Cyrillic = "Cyrillic"; 20 | export const Devanagari = "Devanagari"; 21 | export const Ethiopic = "Ethiopic"; 22 | export const Georgian = "Georgian"; 23 | export const Glagolitic = "Glagolitic"; 24 | export const Greek = "Greek"; 25 | export const Gujarati = "Gujarati"; 26 | export const Gurmukhi = "Gurmukhi"; 27 | export const Han = "Han"; 28 | export const Hangul = "Hangul"; 29 | export const Hanunoo = "Hanunoo"; 30 | export const Hebrew = "Hebrew"; 31 | export const Hiragana = "Hiragana"; 32 | export const Inherited = "Inherited"; 33 | export const Javanese = "Javanese"; 34 | export const Kannada = "Kannada"; 35 | export const Katakana = "Katakana"; 36 | export const KayahLi = "KayahLi"; 37 | export const Khmer = "Khmer"; 38 | export const Lao = "Lao"; 39 | export const Latin = "Latin"; 40 | export const Lepcha = "Lepcha"; 41 | export const Limbu = "Limbu"; 42 | export const Lisu = "Lisu"; 43 | export const Malayalam = "Malayalam"; 44 | export const Mandaic = "Mandaic"; 45 | export const MeeteiMayek = "MeeteiMayek"; 46 | export const Mongolian = "Mongolian"; 47 | export const Myanmar = "Myanmar"; 48 | export const NewTaiLue = "NewTaiLue"; 49 | export const Nko = "Nko"; 50 | export const Ogham = "Ogham"; 51 | export const OlChiki = "OlChiki"; 52 | export const Oriya = "Oriya"; 53 | export const PhagsPa = "PhagsPa"; 54 | export const Rejang = "Rejang"; 55 | export const Runic = "Runic"; 56 | export const Samaritan = "Samaritan"; 57 | export const Saurashtra = "Saurashtra"; 58 | export const Sinhala = "Sinhala"; 59 | export const Sundanese = "Sundanese"; 60 | export const SylotiNagri = "SylotiNagri"; 61 | export const Syriac = "Syriac"; 62 | export const Tagalog = "Tagalog"; 63 | export const Tagbanwa = "Tagbanwa"; 64 | export const TaiLe = "TaiLe"; 65 | export const TaiTham = "TaiTham"; 66 | export const TaiViet = "TaiViet"; 67 | export const Tamil = "Tamil"; 68 | export const Telugu = "Telugu"; 69 | export const Thaana = "Thaana"; 70 | export const Thai = "Thai"; 71 | export const Tibetan = "Tibetan"; 72 | export const Tifinagh = "Tifinagh"; 73 | export const Vai = "Vai"; 74 | export const Yi = "Yi"; 75 | } 76 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/then.ts: -------------------------------------------------------------------------------- 1 | import type { ImplicitParjser, ParjsCombinator } from "../../index"; 2 | import { ResultKind } from "../result"; 3 | import type { ParsingState } from "../state"; 4 | 5 | import type { CombinatorInput } from "../combinated"; 6 | import { Combinated } from "../combinated"; 7 | import { wrapImplicit } from "../wrap-implicit"; 8 | import { composeCombinator } from "./combinator"; 9 | import { map } from "./map"; 10 | 11 | import type { getParsedType } from "../util-types"; 12 | 13 | /** 14 | * Applies the source parser followed by `next`. Yields the result of `next`. 15 | * 16 | * @param next 17 | */ 18 | export function qthen(next: ImplicitParjser): ParjsCombinator { 19 | return composeCombinator( 20 | then(next), 21 | map(arr => arr[1]) 22 | ); 23 | } 24 | 25 | /** 26 | * Applies the source parser followed by `next`. Yields the result of the source parser. 27 | * 28 | * @param next 29 | */ 30 | export function thenq(next: ImplicitParjser): ParjsCombinator { 31 | return composeCombinator( 32 | then(next), 33 | map(arr => arr[0]) 34 | ); 35 | } 36 | 37 | class Then[]> extends Combinated< 38 | T, 39 | [ 40 | T, 41 | ...{ 42 | [K in keyof Rest]: getParsedType; 43 | } 44 | ] 45 | > { 46 | type = "then"; 47 | expecting = this.source.expecting; 48 | private _seq = [this.source, ...this._rest]; 49 | 50 | constructor( 51 | source: CombinatorInput, 52 | private _rest: Rest 53 | ) { 54 | super(source); 55 | } 56 | 57 | _apply(ps: ParsingState): void { 58 | const results = [] as unknown[]; 59 | const { _seq } = this; 60 | const origPos = ps.position; 61 | for (const cur of _seq) { 62 | cur.apply(ps); 63 | if (ps.isOk) { 64 | results.push(ps.value); 65 | } else if (ps.isSoft && origPos === ps.position) { 66 | // if the first parser failed softly then we propagate a soft failure. 67 | return; 68 | } else if (ps.isSoft) { 69 | ps.kind = ResultKind.HardFail; 70 | // if a i > 0 parser failed softly, this is a hard fail for us. 71 | // also, propagate the internal expectation. 72 | return; 73 | } else { 74 | // ps failed hard or fatally. The same severity. 75 | return; 76 | } 77 | } 78 | ps.value = results; 79 | ps.kind = ResultKind.Ok; 80 | } 81 | } 82 | 83 | export function then[]>( 84 | ...parsers: Parsers 85 | ): ParjsCombinator }]> { 86 | const resolvedParsers = parsers.map(wrapImplicit) as { 87 | [K in keyof Parsers]: CombinatorInput>; 88 | }; 89 | return source => { 90 | return new Then(wrapImplicit(source), resolvedParsers); 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/trace.spec.ts: -------------------------------------------------------------------------------- 1 | import type { ParjsFailure } from "@lib"; 2 | import { string, whitespace } from "@lib"; 3 | import { exactly, manySepBy, then } from "@lib/combinators"; 4 | import { visualizeTrace } from "@lib/internal/trace-visualizer"; 5 | 6 | describe("trace", () => { 7 | string("a").pipe(manySepBy(whitespace())); 8 | describe("single line input", () => { 9 | const input = "a".repeat(4); 10 | const res = string("a").pipe(exactly(5)).parse(input) as ParjsFailure; 11 | const { trace } = res; 12 | it("correct position", () => { 13 | expect(trace.position).toEqual(4); 14 | }); 15 | 16 | it("correct kind", () => { 17 | expect(trace.kind).toBe("Hard"); 18 | expect(res.kind).toBe(trace.kind); 19 | expect(res.reason).toBe(trace.reason); 20 | }); 21 | const location = trace.location; 22 | 23 | it("correct input", () => { 24 | expect(trace.input).toBe(input); 25 | }); 26 | 27 | it("correct line 0", () => { 28 | expect(location.line).toBe(0); 29 | }); 30 | 31 | it("correct col", () => { 32 | expect(location.column).toBe(4); 33 | }); 34 | }); 35 | 36 | describe("line breaks \\n", () => { 37 | const input = "\n".repeat(11) + "a".repeat(4); 38 | const parser = whitespace().pipe(then(string("a").pipe(exactly(5)))); 39 | const res = parser.parse(input) as ParjsFailure; 40 | const { trace } = res; 41 | it("correct position", () => { 42 | expect(trace.position).toBe(15); 43 | }); 44 | 45 | it("correct column 4", () => { 46 | expect(trace.location.column).toBe(4); 47 | }); 48 | 49 | it("correct line 11", () => { 50 | expect(trace.location.line).toBe(11); 51 | }); 52 | }); 53 | 54 | describe("line breaks mixed", () => { 55 | const input = "\r\n".repeat(3) + "\r".repeat(3) + "\n".repeat(3) + "a".repeat(4); 56 | const parser = whitespace().pipe(then(string("a").pipe(exactly(5)))); 57 | 58 | const res = parser.parse(input) as ParjsFailure; 59 | const { trace } = res; 60 | it("correct message", () => { 61 | const traceOutput = visualizeTrace.configure({ 62 | lineNumbers: true 63 | })(trace); 64 | expect(traceOutput).toMatchInlineSnapshot(` 65 | "Hard failure at Ln 9 Col 5 66 | 8 | 67 | 9 | aaaa 68 | ^expecting 'a' 69 | 70 | Stack: 71 | expecting 'a' (string) 72 | expecting 'a' (exactly) 73 | expecting a character matching a predicate (then) 74 | " 75 | `); 76 | }); 77 | 78 | it("correct position", () => { 79 | expect(trace.position).toBe(16); 80 | }); 81 | 82 | it("correct column", () => { 83 | expect(trace.location.column).toBe(4); 84 | }); 85 | 86 | it("correct line", () => { 87 | expect(trace.location.line).toBe(8); 88 | }); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /packages/parjs/spec/unit/combinators/then.spec.ts: -------------------------------------------------------------------------------- 1 | import { ResultKind, eof, fail, rest, string } from "@lib"; 2 | import { each, mapConst, then, thenq } from "@lib/combinators"; 3 | 4 | const excessInput = "abcde"; 5 | 6 | describe("then", () => { 7 | describe("1 arg", () => { 8 | const parser = string("ab").pipe(then(string("cd"))); 9 | it("succeeds", () => { 10 | expect(parser.parse("abcd")).toBeSuccessful(["ab", "cd"]); 11 | }); 12 | it("fails softly on first fail", () => { 13 | expect(parser.parse("a")).toBeFailure(ResultKind.SoftFail); 14 | }); 15 | it("fails hard on 2nd fail", () => { 16 | expect(parser.parse("ab")).toBeFailure(ResultKind.HardFail); 17 | }); 18 | it("fails on excess input", () => { 19 | expect(parser.parse(excessInput)).toBeFailure(ResultKind.SoftFail); 20 | }); 21 | 22 | it("fails hard on first hard fail", () => { 23 | const parser2 = fail().pipe(then("hi")); 24 | expect(parser2.parse("hi")).toBeFailure("Hard"); 25 | }); 26 | 27 | it("fails fatally on 2nd fatal fail", () => { 28 | const parser2 = string("hi").pipe( 29 | then( 30 | fail({ 31 | kind: "Fatal" 32 | }) 33 | ) 34 | ); 35 | expect(parser2.parse("hi")).toBeFailure("Fatal"); 36 | }); 37 | 38 | it("chain zero-matching parsers", () => { 39 | const parser2 = string("hi").pipe(then(rest(), rest())); 40 | expect(parser2.parse("hi")).toBeSuccessful(["hi", "", ""]); 41 | }); 42 | }); 43 | 44 | describe("1 arg, then zero consume", () => { 45 | const parser = string("ab").pipe(then(string("cd")), thenq(eof())); 46 | it("succeeds", () => { 47 | expect(parser.parse("abcd")).toBeSuccessful(["ab", "cd"]); 48 | }); 49 | it("fails hard when 3rd fails", () => { 50 | expect(parser.parse(excessInput)).toBeFailure(ResultKind.HardFail); 51 | }); 52 | }); 53 | 54 | it("2 args", () => { 55 | const p2 = string("b").pipe(mapConst(1)); 56 | const p3 = string("c").pipe(mapConst([] as string[])); 57 | 58 | const p = string("a").pipe( 59 | then(p2, p3), 60 | each(x => { 61 | Math.log(x[1]); 62 | x[0].toUpperCase(); 63 | x[2].map(xx => xx.toUpperCase()); 64 | }) 65 | ); 66 | 67 | expect(p.parse("abc")).toBeSuccessful(["a", 1, []]); 68 | }); 69 | 70 | it("3 args", () => { 71 | const p2 = string("b").pipe(mapConst(1)); 72 | const p3 = string("c").pipe(mapConst([] as string[])); 73 | 74 | const p4 = string("d").pipe(mapConst(true)); 75 | 76 | const p = string("a").pipe( 77 | then(p2, p3, p4), 78 | each(x => { 79 | Math.log(x[1]); 80 | x[0].toUpperCase(); 81 | x[2].map(xx => xx.toUpperCase()); 82 | }) 83 | ); 84 | 85 | expect(p.parse("abcd")).toBeSuccessful(["a", 1, [], true]); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /packages/parjs/examples/spec/index.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@jest/globals"; 2 | import type { MatcherFunction, SyncExpectationResult } from "expect"; 3 | import type { ResultKind } from "parjs"; 4 | import { isParjsFailure, isParjsResult, isParjsSuccess } from "parjs/internal"; 5 | 6 | // helper 7 | const fail = (message: string): SyncExpectationResult => ({ 8 | pass: false, 9 | message: () => message 10 | }); 11 | 12 | export const toBeSuccessful: MatcherFunction<[value: unknown]> = 13 | // jest recommends to type the parameters as `unknown` and to validate the values 14 | function (actual: unknown, expected: unknown): SyncExpectationResult { 15 | if (!isParjsResult(actual)) { 16 | throw new Error( 17 | `toBeSuccessful must be called on a ParjsResult, but was called on ${typeof actual}` 18 | ); 19 | } 20 | 21 | if (!isParjsSuccess(actual)) { 22 | return fail(`expected the parse result to be a ParjsSuccess instance:\n\n${actual}`); 23 | } 24 | 25 | const actualString = JSON.stringify(actual, null, 2); 26 | if (actual.kind !== "OK") { 27 | return fail( 28 | `expected the parse result ${actualString} to have kind 'OK' but it had kind '${actual.kind}'` 29 | ); 30 | } 31 | 32 | if (expected !== undefined) { 33 | try { 34 | // check the structure of the objects, not their references 35 | expect(actual.value).toEqual(expected); 36 | } catch (error) { 37 | return fail(`Unexpected parser result value. \n\n${error}`); 38 | } 39 | } 40 | 41 | return { 42 | pass: true, 43 | message() { 44 | return `toBeSuccessful succeeded 👍`; 45 | } 46 | }; 47 | }; 48 | 49 | export const toBeFailure: MatcherFunction<[kind?: string]> = function ( 50 | actual: unknown, 51 | expected: unknown 52 | ): SyncExpectationResult { 53 | if (!isParjsResult(actual)) { 54 | throw new Error("toBeFailure must be called on a ParjsResult"); 55 | } 56 | 57 | const actualString = JSON.stringify(actual, null, 2); 58 | 59 | if (!isParjsFailure(actual)) { 60 | return fail( 61 | `expected the parse result ${actualString} to be a ParjsFailure instance, but its type is '${typeof actual}'` 62 | ); 63 | } 64 | 65 | // if a kind was specified, check it 66 | if (expected !== undefined && actual.kind !== expected) { 67 | return fail( 68 | `expected the parse result ${actualString} to have kind '${expected}' but it had kind '${actual.kind}'` 69 | ); 70 | } 71 | 72 | return { 73 | pass: true, 74 | message() { 75 | return `toBeFailure succeeded 👍`; 76 | } 77 | }; 78 | }; 79 | 80 | expect.extend({ 81 | toBeSuccessful, 82 | toBeFailure 83 | }); 84 | 85 | declare global { 86 | // eslint-disable-next-line ts/no-namespace 87 | export namespace jest { 88 | export interface Matchers { 89 | toBeSuccessful(value?: T): R; 90 | toBeFailure(kind?: ResultKind): R; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/parjs/spec/utilities/index.ts: -------------------------------------------------------------------------------- 1 | import { expect } from "@jest/globals"; 2 | import type { ResultKind } from "@lib"; 3 | import { isParjsFailure, isParjsResult, isParjsSuccess } from "@lib/internal"; 4 | import type { MatcherFunction, SyncExpectationResult } from "expect"; 5 | 6 | // helper 7 | const fail = (message: string): SyncExpectationResult => ({ 8 | pass: false, 9 | message: () => message 10 | }); 11 | 12 | export const toBeSuccessful: MatcherFunction<[value: unknown]> = 13 | // jest recommends to type the parameters as `unknown` and to validate the values 14 | function (actual: unknown, expected: unknown): SyncExpectationResult { 15 | if (!isParjsResult(actual)) { 16 | throw new Error( 17 | `toBeSuccessful must be called on a ParjsResult, but was called on ${typeof actual}` 18 | ); 19 | } 20 | 21 | if (!isParjsSuccess(actual)) { 22 | return fail(`expected the parse result to be a ParjsSuccess instance:\n\n${actual}`); 23 | } 24 | 25 | const actualString = JSON.stringify(actual, null, 2); 26 | if (actual.kind !== "OK") { 27 | return fail( 28 | `expected the parse result ${actualString} to have kind 'OK' but it had kind '${actual.kind}'` 29 | ); 30 | } 31 | 32 | if (expected !== undefined) { 33 | try { 34 | // check the structure of the objects, not their references 35 | expect(actual.value).toEqual(expected); 36 | } catch (error) { 37 | return fail(`Unexpected parser result value. \n\n${error}`); 38 | } 39 | } 40 | 41 | return { 42 | pass: true, 43 | message() { 44 | return `toBeSuccessful succeeded 👍`; 45 | } 46 | }; 47 | }; 48 | 49 | export const toBeFailure: MatcherFunction<[kind?: string]> = function ( 50 | actual: unknown, 51 | expected: unknown 52 | ): SyncExpectationResult { 53 | if (!isParjsResult(actual)) { 54 | throw new Error("toBeFailure must be called on a ParjsResult"); 55 | } 56 | 57 | const actualString = JSON.stringify(actual, null, 2); 58 | 59 | if (!isParjsFailure(actual)) { 60 | return fail( 61 | `expected the parse result ${actualString} to be a ParjsFailure instance, but its type is '${typeof actual}'` 62 | ); 63 | } 64 | 65 | // if a kind was specified, check it 66 | if (expected !== undefined && actual.kind !== expected) { 67 | return fail( 68 | `expected the parse result ${actualString} to have kind '${expected}' but it had kind '${actual.kind}'` 69 | ); 70 | } 71 | 72 | return { 73 | pass: true, 74 | message() { 75 | return `toBeFailure succeeded 👍`; 76 | } 77 | }; 78 | }; 79 | 80 | expect.extend({ 81 | toBeSuccessful, 82 | toBeFailure 83 | }); 84 | 85 | declare global { 86 | // eslint-disable-next-line ts/no-namespace 87 | export namespace jest { 88 | export interface Matchers { 89 | toBeSuccessful(value?: T): R; 90 | toBeFailure(kind?: ResultKind): R; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/char-info/README.md: -------------------------------------------------------------------------------- 1 | # char-info 2 | 3 | ![build](https://github.com/GregRos/parjs/actions/workflows/char-info.push.yaml/badge.svg) 4 | [![codecov](https://codecov.io/github/GregRos/parjs/graph/badge.svg?flag=char-info)](https://codecov.io/github/GregRos/parjs?flags[0]=char-info) 5 | [![npm](https://img.shields.io/npm/v/char-info)](https://www.npmjs.com/package/char-info) 6 | [![Downloads](https://img.shields.io/npm/dm/char-info)](https://www.npmjs.com/package/char-info) 7 | [![Gzipped Size](https://img.shields.io/bundlephobia/minzip/char-info)](https://bundlephobia.com/result?p=char-info) 8 | 9 | A library that gives you information about individual Unicode characters. It also provides a list of Unicode groupings and their names, including lists of categories, blocks, and scripts. This is also reflected in the library's type definition files. 10 | 11 | You can use for stuff like: 12 | 13 | 1. Find out what language a character is in, such as Greek (α), Latin (a), Hebrew (א), and so on. 14 | 2. Whether it's a kind of punctuation, digit, letter, emoji, spacing mark, or something else 15 | 3. What Unicode character block it inhabits 16 | 4. If it's upper-case or lower-case 17 | 18 | The library only supports characters in the BMP. There are no plans to expanding it beyond the BMP. 19 | 20 | In addition, the library provides basic ASCII character indicator functions, such as `isLetter`, `isUpper`, and so forth. These have really simple implementations and don't have the overhead of the Unicode indicators. 21 | 22 | The library comes bundled with Unicode character information tables that originally came from the Unicode Character Database. This data can make bundles with the library being quite heavy. To counteract this, the library is built to leverage "tree shaking" or dead code elimination, which is used in all modern bundlers. Provided you only import the members you'll use, only some of the data will end up in your bundle. For example, if you just import the ASCII character indicators your bundle won't contain any Unicode data at all. 23 | 24 | Enabling dead code elimination may require switching to ES2015 native modules. You'll need to look at your bundler's documentation for more information. 25 | 26 | ## Imports 27 | 28 | The package has three paths you can import from: 29 | 30 | 1. `char-info/ascii`, which contains indicator functions for ASCII characters and character codes. 31 | 2. `char-info/unicode`, which contain the special Unicode character indicators. 32 | 3. `char-info`, which re-exports everything. 33 | 34 | If you're using a properly configured bundler, you can import what you need from `char-info` and still take advantage of tree shaking. 35 | 36 | ## Usage - ASCII indicators 37 | 38 | ```typescript 39 | import {isLetter, isUpper, isUpperCode} from "char-info" 40 | 41 | assert.isTrue(isLetter("a")); 42 | assert.isTrue(isUpper("A")); 43 | assert.isTrue(isUpperCode("A")); 44 | 45 | // You can also do it like this: 46 | import * as AsciiInfo from "char-info/ascii"; 47 | 48 | assert.isTrue(AsciiInfo.isLetterCode("a".charCodeAt(0)); 49 | 50 | ``` 51 | 52 | ## Usage - Unicode Indicators 53 | 54 | Unicode indicators work a bit differently, and aren't just plain functions. Instead, each indicator has two members, `char` and `code`, for testing characters (in the form of strings) and character codes, respectively. 55 | 56 | ```typescript 57 | import { uniIsLetter, uniIsDigit } from "char-info"; 58 | 59 | assert.isTrue(uniIsLetter.char("א")); 60 | assert.isTrue(uniIsDigit.char("٩")); 61 | ``` 62 | -------------------------------------------------------------------------------- /packages/char-info/src/unicode.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Unicode character indicators. 3 | * 4 | * @module char-info/unicode 5 | * @preferred 6 | */ import type { CharClassIndicator } from "./indicator-type"; 7 | import { BasicCharClassIndicator } from "./indicators"; 8 | import { UnicodeCategory } from "./names"; 9 | import { lookup } from "./unicode-lookup"; 10 | 11 | function homogenizeInputStr(str: string) { 12 | return str.toLowerCase().replace(/[_ -,]/g, ""); 13 | } 14 | 15 | /** 16 | * Returns an indicator for characters belonging to the Unicode category, `category`. 17 | * 18 | * @param category The category to test for. 19 | */ 20 | export function uniInCategory(category: string): CharClassIndicator { 21 | category = homogenizeInputStr(category); 22 | if (category.length > 3) { 23 | category = lookup.longCategoryToCode.get(category)!; 24 | } 25 | const uGroup = lookup.categories.get(category); 26 | return new BasicCharClassIndicator(uGroup!); 27 | } 28 | 29 | /** 30 | * Returns an indicator for characters belonging to a Unicode script, `script`. 31 | * 32 | * @param script 33 | */ 34 | export function uniInScript(script: string) { 35 | script = homogenizeInputStr(script); 36 | const uGroup = lookup.scripts.get(script); 37 | return new BasicCharClassIndicator(uGroup!); 38 | } 39 | 40 | /** 41 | * Returns an indicator for characters belonging to the Unicode block, `block`. 42 | * 43 | * @param block 44 | */ 45 | export function uniInBlock(block: string) { 46 | block = homogenizeInputStr(block); 47 | const uGroup = lookup.blocks.get(block); 48 | return new BasicCharClassIndicator(uGroup!); 49 | } 50 | 51 | /** Indicator for Unicode decimal digit characters. */ 52 | export const uniIsDecimal = uniInCategory(UnicodeCategory.NumberDecimalDigit); 53 | 54 | /** Indicator for Unicode letters. */ 55 | export const uniIsLetter = uniInCategory(UnicodeCategory.Letter); 56 | 57 | /** Indicator for Unicode lowercase letters. */ 58 | export const uniIsLower = uniInCategory(UnicodeCategory.LetterLowercase); 59 | 60 | /** Indicator for Unicode uppercase letters. */ 61 | export const uniIsUpper = uniInCategory(UnicodeCategory.LetterUppercase); 62 | 63 | /** Indicator for Unicode inline spaces. */ 64 | export const uniIsSpace = uniInCategory(UnicodeCategory.SeparatorSpace); 65 | 66 | /** Indicator for Unicode vertical separators. */ 67 | export const uniIsNewline = uniInCategory(UnicodeCategory.Custom_SeparatorVertical); 68 | 69 | /** Returns the Unicode categories for a character or code. */ 70 | export const uniGetCategories = { 71 | code(code: number) { 72 | return lookup.allCategories.search(code, code); 73 | }, 74 | char(char: string) { 75 | return uniGetCategories.code(char.charCodeAt(0)); 76 | } 77 | }; 78 | 79 | /** Returns the Unicode scripts for a character or code. */ 80 | export const uniGetScripts = { 81 | code(code: number) { 82 | return lookup.allScripts.search(code, code); 83 | }, 84 | char(char: string) { 85 | return uniGetScripts.code(char.charCodeAt(0)); 86 | } 87 | }; 88 | 89 | /** Returns the Unicode block for a character or code. */ 90 | export const uniGetBlock = { 91 | code(code: number) { 92 | return lookup.allBlocks.search(code, code)[0]; 93 | }, 94 | char(char: string) { 95 | return uniGetBlock.code(char.charCodeAt(0)); 96 | } 97 | }; 98 | 99 | export { UnicodeBlock, UnicodeCategory, UnicodeScript } from "./names"; 100 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/result.ts: -------------------------------------------------------------------------------- 1 | import { ParjsParsingFailure } from "../errors"; 2 | import type { Parjser } from "./parjser"; 3 | import { visualizeTrace } from "./trace-visualizer"; 4 | 5 | /** Indicates a success reply and contains the value and other information. */ 6 | export class ParjsSuccess implements SuccessInfo { 7 | /** The kind of the result: OK, Soft, Hard, Fatal. */ 8 | kind = ResultKind.Ok; 9 | 10 | constructor(public value: T) {} 11 | 12 | toString() { 13 | return `Success: ${this.value}`; 14 | } 15 | 16 | /** Whether this result is an OK. */ 17 | get isOk() { 18 | return true; 19 | } 20 | } 21 | 22 | /** Info about a success. */ 23 | export interface SuccessInfo { 24 | kind: ResultKindOk; 25 | value: T; 26 | } 27 | 28 | /** Info about a potential failure. */ 29 | export interface FailureInfo { 30 | kind: ResultKindFail; 31 | reason: string; 32 | } 33 | 34 | /** The line and column of where a failure happened. */ 35 | export interface ErrorLocation { 36 | line: number; 37 | column: number; 38 | } 39 | 40 | /** An object indicating trace information about the state of parsing when it was stopped. */ 41 | export interface Trace extends FailureInfo { 42 | userState: object; 43 | position: number; 44 | location: ErrorLocation; 45 | stackTrace: Parjser[]; 46 | input: string; 47 | } 48 | 49 | /** A failure result from a Parjs parser. */ 50 | export class ParjsFailure implements FailureInfo { 51 | constructor(public trace: Trace) {} 52 | 53 | get value(): never { 54 | throw new ParjsParsingFailure(this); 55 | } 56 | 57 | get kind() { 58 | return this.trace.kind; 59 | } 60 | 61 | get reason() { 62 | return this.trace.reason; 63 | } 64 | 65 | toString() { 66 | return visualizeTrace(this.trace); 67 | } 68 | /** Whether this result is an OK. */ 69 | get isOk() { 70 | return false; 71 | } 72 | } 73 | 74 | export function isParjsSuccess(x: unknown): x is ParjsSuccess { 75 | return x instanceof ParjsSuccess; 76 | } 77 | 78 | export function isParjsFailure(x: unknown): x is ParjsFailure { 79 | return x instanceof ParjsFailure; 80 | } 81 | 82 | export function isParjsResult(x: unknown): x is ParjsResult { 83 | return x instanceof ParjsSuccess || x instanceof ParjsFailure; 84 | } 85 | 86 | /** A type that represents a ParjsSuccess or a ParjsFailure. Returned by parsers. */ 87 | export type ParjsResult = ParjsSuccess | ParjsFailure; 88 | 89 | /** Namespace that contains the different reply kinds/error levels. */ 90 | export const ResultKind = { 91 | /** An OK reply. */ 92 | Ok: "OK" as const, 93 | /** A soft failure reply. */ 94 | SoftFail: "Soft" as const, 95 | /** A hard failure reply. */ 96 | HardFail: "Hard" as const, 97 | /** A fatal failure reply. */ 98 | FatalFail: "Fatal" as const 99 | }; 100 | 101 | /** The OK reply type. */ 102 | export type ResultKindOk = typeof ResultKind.Ok; 103 | /** The soft failure type. */ 104 | export type ResultKindSoftFail = typeof ResultKind.SoftFail; 105 | /** The hard failure type. */ 106 | export type ResultKindHardFail = typeof ResultKind.HardFail; 107 | /** The fatal failure type. */ 108 | export type ResultKindFatalFail = typeof ResultKind.FatalFail; 109 | 110 | /** Specifies any kind of failure. */ 111 | export type ResultKindFail = ResultKindHardFail | ResultKindFatalFail | ResultKindSoftFail; 112 | 113 | /** Specifies a reply kind, indicating success or failure, and the severity of the failure. */ 114 | export type ResultKind = ResultKindOk | ResultKindFail; 115 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/state.ts: -------------------------------------------------------------------------------- 1 | import { ResultKind } from "./result"; 2 | 3 | /** Container type for user state data. */ 4 | export interface UserState { 5 | [key: string]: unknown; 6 | } 7 | 8 | /** Maintains progress for parsing a single input. */ 9 | export interface ParsingState { 10 | /** The original string input on which parsing is performed. Should not be mutated while parsing. */ 11 | readonly input: string; 12 | /** The next character waiting to be parsed. */ 13 | position: number; 14 | /** The value from the last parser action performed on this state. */ 15 | value: unknown; 16 | /** Additional state data. */ 17 | userState: UserState; 18 | 19 | /** Initial user state. */ 20 | readonly initialUserState: UserState | undefined; 21 | 22 | /** A stack that indicates entered parsers. Should not be modified by user code. */ 23 | stack: { 24 | type: string; 25 | expecting: string; 26 | }[]; 27 | 28 | /** 29 | * If the result is a failure, this field will indicate the reason for the failure. If the 30 | * result is OK, this must be undefined. 31 | */ 32 | reason: string | undefined; 33 | /** The result of the last parser action: OK, SoftFailure, HardFailure, FatalFailure. */ 34 | kind: ResultKind; 35 | /** Shorthand for this.result == Okay */ 36 | readonly isOk: boolean; 37 | /** Shorthand for this.result == SoftFailure */ 38 | readonly isSoft: boolean; 39 | /** Shorthand for this.result == HardFailure */ 40 | readonly isHard: boolean; 41 | /** Shorthand for this.result == FatalFailure */ 42 | readonly isFatal: boolean; 43 | 44 | atLeast(kind: ResultKind): boolean | undefined; 45 | 46 | atMost(kind: ResultKind): boolean | undefined; 47 | } 48 | 49 | function worseThan(a: ResultKind, b: ResultKind): boolean | undefined { 50 | if (a === ResultKind.Ok) { 51 | return b === ResultKind.Ok; 52 | } 53 | if (a === ResultKind.SoftFail) { 54 | return b === ResultKind.SoftFail || b === ResultKind.Ok; 55 | } 56 | if (a === ResultKind.HardFail) { 57 | return b !== ResultKind.FatalFail; 58 | } 59 | if (a === ResultKind.FatalFail) { 60 | return true; 61 | } 62 | } 63 | 64 | /** Basic implementation of the ParsingState interface. */ 65 | export class BasicParsingState implements ParsingState { 66 | position = 0; 67 | stack = []; 68 | initialUserState: UserState | undefined = undefined; 69 | value: TValue | undefined = undefined; 70 | kind!: ResultKind; 71 | reason!: string; 72 | 73 | constructor( 74 | public input: string, 75 | public userState: UserState 76 | ) {} 77 | 78 | get isOk() { 79 | return this.kind === ResultKind.Ok; 80 | } 81 | 82 | get isSoft() { 83 | return this.kind === ResultKind.SoftFail; 84 | } 85 | 86 | get isHard() { 87 | return this.kind === ResultKind.HardFail; 88 | } 89 | 90 | get isFatal() { 91 | return this.kind === ResultKind.FatalFail; 92 | } 93 | 94 | atLeast(kind: ResultKind) { 95 | return worseThan(this.kind, kind); 96 | } 97 | 98 | atMost(kind: ResultKind) { 99 | return worseThan(kind, this.kind); 100 | } 101 | } 102 | 103 | /** A unique object value indicating the reuslt of a failed parser. */ 104 | export const FAIL_RESULT = Object.create(null); 105 | /** 106 | * A unique object value indicating that a parser did not initialize the ParsingState's value 107 | * property before terminating, which is an error. 108 | */ 109 | export const UNINITIALIZED_RESULT = Object.create(null); 110 | 111 | // tslint:enable:naming-convention 112 | -------------------------------------------------------------------------------- /packages/parjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parjs", 3 | "version": "1.3.9", 4 | "description": "Library for building parsers using combinators.", 5 | "keywords": [ 6 | "parser", 7 | "parsing", 8 | "parser combinator", 9 | "parser-combinator", 10 | "functional", 11 | "typescript", 12 | "text", 13 | "string", 14 | "language", 15 | "combinator", 16 | "parsec", 17 | "fparsec", 18 | "parsimmon" 19 | ], 20 | "homepage": "https://parjs.org", 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/gregros/parjs.git", 24 | "directory": "packages/parjs" 25 | }, 26 | "license": "MIT", 27 | "author": "GregRos ", 28 | "contributors": [ 29 | { 30 | "name": "GregRos", 31 | "email": "work.gregr@gmail.com" 32 | }, 33 | { 34 | "name": "Mika Vilpas", 35 | "email": "mika.vilpas@gmail.com" 36 | }, 37 | { 38 | "name": "Maarten", 39 | "email": "digitalheir@users.noreply.github.com" 40 | }, 41 | { 42 | "name": "Maxime Mangel", 43 | "email": "mangel.maxime@protonmail.com" 44 | }, 45 | { 46 | "name": "Paul Gowder", 47 | "email": "paultopia@users.noreply.github.com" 48 | }, 49 | { 50 | "name": "Raidou", 51 | "email": "weirongxu.raidou@gmail.com" 52 | }, 53 | { 54 | "name": "Xavier Maso", 55 | "email": "xavier.maso@oracle.com" 56 | }, 57 | { 58 | "name": "sheey11", 59 | "email": "13375959+sheey11@users.noreply.github.com" 60 | }, 61 | { 62 | "name": "Collider LI", 63 | "email": "lhc199652@gmail.com" 64 | }, 65 | { 66 | "name": "matlin" 67 | } 68 | ], 69 | "sideEffects": false, 70 | "exports": { 71 | ".": { 72 | "require": "./dist/index.js", 73 | "import": "./dist/index.js", 74 | "default": "./dist/index.js", 75 | "types": "./dist/index.d.ts" 76 | }, 77 | "./combinators": { 78 | "require": "./dist/combinators.js", 79 | "import": "./dist/combinators.js", 80 | "default": "./dist/combinators.js", 81 | "types": "./dist/combinators.d.ts" 82 | }, 83 | "./internal": { 84 | "require": "./dist/internal.js", 85 | "import": "./dist/internal.js", 86 | "default": "./dist/internal.js", 87 | "types": "./dist/internal.d.ts" 88 | } 89 | }, 90 | "main": "dist/index.js", 91 | "typesVersions": { 92 | "*": { 93 | ".": [ 94 | "./dist/index.d.ts" 95 | ], 96 | "combinators": [ 97 | "./dist/combinators.d.ts" 98 | ], 99 | "internal": [ 100 | "./dist/internal.d.ts" 101 | ] 102 | } 103 | }, 104 | "typings": "dist/index", 105 | "files": [ 106 | "src", 107 | "dist", 108 | "examples", 109 | "package.json", 110 | "CHANGELOG.md", 111 | "LICENSE.md", 112 | "README.md" 113 | ], 114 | "scripts": { 115 | "build": "tsc -b .", 116 | "build:clean": "run-s clean build", 117 | "clean": "shx rm -rf dist", 118 | "test": "jest", 119 | "test:coverage": "jest --coverage" 120 | }, 121 | "nyc": { 122 | "include": [ 123 | "dist/**/*.js" 124 | ], 125 | "produce-source-map": true, 126 | "reporter": [ 127 | "lcov", 128 | "text-summary" 129 | ], 130 | "sourceMap": true 131 | }, 132 | "dependencies": { 133 | "char-info": "0.3.*" 134 | }, 135 | "devDependencies": { 136 | "@swc/core": "1.3.96", 137 | "@swc/jest": "0.2.29", 138 | "@types/jest": "^29.5.12", 139 | "@types/node": "20.9.1", 140 | "jest": "^29.7.0", 141 | "npm-run-all": "^4.1.5", 142 | "typescript": "^5.4.5" 143 | }, 144 | "packageManager": "yarn@4.1.1", 145 | "engines": { 146 | "node": ">=16.0.0" 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/many-sep-by.ts: -------------------------------------------------------------------------------- 1 | import type { ImplicitParjser, ParjsCombinator } from "../../index"; 2 | import type { CombinatorInput } from "../combinated"; 3 | import { Combinated } from "../combinated"; 4 | import { Issues } from "../issues"; 5 | import type { ParjserBase } from "../parser"; 6 | import { ResultKind } from "../result"; 7 | import type { ParsingState } from "../state"; 8 | import { wrapImplicit } from "../wrap-implicit"; 9 | 10 | export type ArrayWithSeparators = Normal[] & { 11 | separators: Separator[]; 12 | }; 13 | 14 | export function getArrayWithSeparators( 15 | things: T[], 16 | separators: S[] 17 | ): ArrayWithSeparators { 18 | const array = things as ArrayWithSeparators; 19 | array.separators = separators; 20 | return array; 21 | } 22 | 23 | export interface ManySepByOptions { 24 | delimeter: CombinatorInput; 25 | max?: number; 26 | } 27 | 28 | class ManySepBy extends Combinated> { 29 | type = "manySepBy"; 30 | expecting = this.source.expecting; 31 | constructor( 32 | source: CombinatorInput, 33 | private readonly _options: ManySepByOptions 34 | ) { 35 | super(source); 36 | } 37 | _apply(ps: ParsingState): void { 38 | const results: ArrayWithSeparators = getArrayWithSeparators([], []); 39 | const { 40 | source, 41 | _options: { delimeter, max = Infinity } 42 | } = this; 43 | source.apply(ps); 44 | if (ps.atLeast(ResultKind.HardFail)) { 45 | return; 46 | } else if (ps.isSoft) { 47 | ps.value = results; 48 | ps.kind = ResultKind.Ok; 49 | return; 50 | } 51 | let { position } = ps; 52 | results.push(ps.value as E); 53 | let i = 1; 54 | for (;;) { 55 | if (i >= max) break; 56 | delimeter.apply(ps); 57 | if (ps.isSoft) { 58 | break; 59 | } else if (ps.atLeast(ResultKind.HardFail)) { 60 | return; 61 | } 62 | results.separators.push(ps.value as Sep); 63 | source.apply(ps); 64 | if (ps.isSoft) { 65 | break; 66 | } else if (ps.atLeast(ResultKind.HardFail)) { 67 | return; 68 | } 69 | if (max >= Infinity && ps.position === position) { 70 | Issues.guardAgainstInfiniteLoop("manySepBy"); 71 | } 72 | results.push(ps.value as E); 73 | position = ps.position; 74 | i++; 75 | } 76 | ps.kind = ResultKind.Ok; 77 | ps.position = position; 78 | ps.value = results; 79 | } 80 | } 81 | 82 | /** 83 | * Applies the source parser repeatedly until it fails softly, with each pair of applications 84 | * separated by applying `delimeter`. Also terminates if `delimeter` fails softly. Yields all the 85 | * results of the source parser in an array. 86 | * 87 | * @template E The type of the source parser. 88 | * @template Sep The type of the delimeter (separator) parser. 89 | * @param delimeter Parser that separates two applications of the source. 90 | * @param max Optionally, then maximum number of times to apply the source parser. Defaults to 91 | * `Infinity`. 92 | */ 93 | export function manySepBy( 94 | delimeter: ImplicitParjser, 95 | max?: number 96 | ): ParjsCombinator>; 97 | 98 | export function manySepBy(implDelimeter: ImplicitParjser, max = Infinity) { 99 | const delimeter = wrapImplicit(implDelimeter) as ParjserBase; 100 | return (source: ImplicitParjser) => { 101 | return new ManySepBy(wrapImplicit(source), { delimeter, max }); 102 | }; 103 | } 104 | -------------------------------------------------------------------------------- /packages/char-info/src/unicode-lookup.ts: -------------------------------------------------------------------------------- 1 | /** @external */ /* tslint:disable:naming-convention */ 2 | 3 | import type { Interval } from "node-interval-tree"; 4 | import DataIntervalTree from "node-interval-tree"; 5 | 6 | import blocks from "./data/block.ranges"; 7 | import categories from "./data/category.ranges"; 8 | import scripts from "./data/script.ranges"; 9 | 10 | export interface UnicodeCharGroup { 11 | name: string; 12 | alias?: string; 13 | intervals: Interval[]; 14 | displayName: string; 15 | } 16 | 17 | export interface UnicodeLookup { 18 | allBlocks: DataIntervalTree; 19 | allCategories: DataIntervalTree; 20 | allScripts: DataIntervalTree; 21 | blocks: Map; 22 | categories: Map; 23 | scripts: Map; 24 | longCategoryToCode: Map; 25 | } 26 | 27 | function homogenizeRawStr(str: string) { 28 | return str.toLowerCase().replace(/_/g, ""); 29 | } 30 | 31 | const rangeRegex = /(\\\w[0-9a-fA-F]+|[\s\S])(?:-(\\\w[0-9a-fA-F]+|[\s\S]))?/g; 32 | 33 | function getCharCode(str: string) { 34 | if (str.length === 1) { 35 | return str.charCodeAt(0); 36 | } 37 | const hex = str.slice(2); 38 | return Number.parseInt(hex, 16); 39 | } 40 | 41 | function expandIntoRanges(compressedForm: string) { 42 | const matches = []; 43 | let x = null; 44 | while ((x = rangeRegex.exec(compressedForm))) { 45 | matches.push([x[1], x[2] || x[1]]); 46 | } 47 | const ranges = []; 48 | 49 | for (const match of matches) { 50 | const start = getCharCode(match[0]); 51 | const end = getCharCode(match[1]); 52 | ranges.push({ 53 | low: start, 54 | high: end 55 | } as Interval); 56 | } 57 | ranges.sort((a, b) => a.low - b.low); 58 | return ranges; 59 | } 60 | 61 | type RawUnicodeName = string | [string, string]; 62 | 63 | type RawUnicodeRecord = [RawUnicodeName, string]; 64 | 65 | function expandRawRecord(raw: RawUnicodeRecord) { 66 | let name: string; 67 | let alias: string | undefined = undefined; 68 | const fst = raw[0]; 69 | if (fst.constructor === Array) { 70 | name = fst[0]; 71 | alias = fst[1]; 72 | } else { 73 | name = fst as string; 74 | } 75 | return { 76 | name, 77 | alias, 78 | intervals: expandIntoRanges(raw[1]), 79 | get displayName() { 80 | return this.alias || this.name; 81 | } 82 | } as UnicodeCharGroup; 83 | } 84 | 85 | function buildLookup() { 86 | const lookup: UnicodeLookup = { 87 | allBlocks: new DataIntervalTree(), 88 | allCategories: new DataIntervalTree(), 89 | allScripts: new DataIntervalTree(), 90 | blocks: new Map(), 91 | categories: new Map(), 92 | scripts: new Map(), 93 | longCategoryToCode: new Map() 94 | }; 95 | for (const rawBlock of blocks) { 96 | const block = expandRawRecord(rawBlock as RawUnicodeRecord); 97 | lookup.blocks.set(homogenizeRawStr(block.name), block); 98 | for (const interval of block.intervals) { 99 | lookup.allBlocks.insert(interval.low, interval.high, block); 100 | } 101 | } 102 | for (const rawCategory of categories) { 103 | const cat = expandRawRecord(rawCategory as RawUnicodeRecord); 104 | const hName = homogenizeRawStr(cat.name); 105 | lookup.categories.set(hName, cat); 106 | lookup.longCategoryToCode.set(homogenizeRawStr(cat.alias!), hName); 107 | for (const interval of cat.intervals) { 108 | lookup.allCategories.insert(interval.low, interval.high, cat); 109 | } 110 | } 111 | for (const rawScript of scripts) { 112 | const script = expandRawRecord(rawScript as RawUnicodeRecord); 113 | lookup.scripts.set(homogenizeRawStr(script.name), script); 114 | for (const interval of script.intervals) { 115 | lookup.allScripts.insert(interval.low, interval.high, script); 116 | } 117 | } 118 | return lookup; 119 | } 120 | 121 | export const lookup = buildLookup(); 122 | -------------------------------------------------------------------------------- /documentation/using-parjs.md: -------------------------------------------------------------------------------- 1 | # Using Parjs 2 | 3 | This file describes workflows that can be used to work with Parjs. 4 | 5 | ## Inspecting the behaviours of parsers with `.debug()` 6 | 7 | The [debug() method](https://gregros.github.io/parjs/interfaces/index.Parjser.html#debug) can be called on any parser (even for the building blocks inside a complex parser!). It will return the parser unchanged, but it will log information about the parser to the console. This works well when you want to inspect what a parser does. 8 | 9 | Here is an example using the `product` parser from the [math example](../src/examples/math.ts): 10 | 11 | ```ts 12 | // simplified example 13 | import { product } from "../../examples/math"; 14 | expect(product().debug().parse("1 * 2 ")).toBeSuccessful(expected); 15 | ``` 16 | 17 | It will log something like this to the console: 18 | 19 | ```txt 20 | consumed '1 * 2 ' (length 6) 21 | at position 0->6 22 | 👍🏻 (Ok) 23 | { 24 | "input": "1 * 2 ", 25 | "userState": {}, 26 | "position": 6, 27 | "stack": [], 28 | "value": { 29 | "lhs": { 30 | "value": 1 31 | }, 32 | "operator": "*", 33 | "rhs": { 34 | "value": 2 35 | } 36 | }, 37 | "kind": "OK", 38 | "reason": "expecting '*' OR expecting '/' OR expecting '%'" 39 | } 40 | { 41 | "type": "map", 42 | "expecting": "product" 43 | } 44 | ``` 45 | 46 | This will help you see: 47 | 48 | - The input that was consumed 49 | - The position in the input 50 | - The value that was parsed (returned by the parser) 51 | 52 | ## Example workflow with a test runner 53 | 54 | Working on your parser by writing tests is an interactive and fun way to develop. Here is a workflow that you can use: 55 | 56 | 1. Write your parser implementation in a file 57 | 2. Write a test file that imports the parser and tests it 58 | 3. Have a test runner that runs the tests and shows the results 59 | 60 | - You can use Jest, Vitest, Mocha, or any other test runner 61 | 62 | Let's write a simple parser that parses a shopping list. The shopping list will have two sections: one for fruit and one for vegetables. Each section will have a list of items. 63 | 64 | Start writing your parser implementation little by little: 65 | 66 | ```ts 67 | // shopping-list.ts 68 | import { string } from "parjs"; 69 | import { many1, manySepBy, or, recover, then } from "parjs/combinators"; 70 | 71 | export const fruitSection = string("Remember to buy ").pipe( 72 | then(string("apples").pipe(or(string("bananas")))) 73 | ); 74 | ``` 75 | 76 | Then write a test file that tests the parser: 77 | 78 | ```ts 79 | // shopping-list.test.ts 80 | import { type ParjsResult } from "parjs"; 81 | import { fruitSection, shoppingList, vegetablesSection } from "./shopping-list"; 82 | 83 | describe("fruitSection", () => { 84 | it("can parse apples", () => { 85 | const result: ParjsResult<["Remember to buy ", "apples" | "bananas"]> = 86 | fruitSection.parse("Remember to buy apples"); 87 | // (^ you can leave out the explicit type in your test code) 88 | 89 | expect(result.isOk).toBe(true); 90 | expect(result.value).toEqual(["Remember to buy ", "apples"]); 91 | }); 92 | 93 | it("can parse bananas", () => { 94 | // NOTE: if you are using jest, you can also simplify the testing logic by 95 | // importing the jest utilities that parjs uses internally. If you have any 96 | // issues, you can just fall back to the simple method described above. 97 | // 98 | // To do this, you need to add the following line to the top of your test 99 | // file: 100 | // 101 | // import "parjs/utilities"; 102 | // 103 | // You can also add it to your test setup file, which is run before any 104 | // tests are executed. 105 | expect(fruitSection.parse("Remember to buy bananas")).toBeSuccessful([ 106 | "Remember to buy ", 107 | "bananas" 108 | ]); 109 | }); 110 | }); 111 | ``` 112 | 113 | Now you can run your tests with your test runner. 114 | 115 | When you add more functionality, you can add more tests. You can also use the `.debug()` method to inspect the behaviour of your parser as described above. If your test runner executes the tests automatically, you can iterate on the parser very quickly. 116 | 117 | Tip: If you have access to an AI assistant such as Github Copilot, you can use it to very quickly generate test cases. This is a great application of AI since the tests are very repetitive and self contained. 118 | -------------------------------------------------------------------------------- /packages/parjs/examples/src/ini.ts: -------------------------------------------------------------------------------- 1 | import type { Parjser } from "parjs"; 2 | import { anyCharOf, eof, newline, result, string } from "parjs"; 3 | import { 4 | between, 5 | many, 6 | many1, 7 | map, 8 | maybe, 9 | or, 10 | qthen, 11 | stringify, 12 | then, 13 | thenq 14 | } from "parjs/combinators"; 15 | 16 | // This is a parser for .ini files. It's a simple format that looks like this: 17 | // 18 | // ; Global section 19 | // abc=def 20 | // ghi=jkl 21 | // 22 | // [SectionName] 23 | // abc=def 24 | // ghi=jkl ; comment 25 | // 26 | // [AnotherSection] 27 | // mno=pqr 28 | // 29 | // The parser is based on the following grammar: 30 | // - Properties: These are key-value pairs, parsed by definitionLine. The keys 31 | // are case-insensitive. 32 | // - Comments: These start with a ; or # and continue to the end of the line. 33 | // They can appear anywhere in the file, including on the same line as a 34 | // property. 35 | // - Sections: These are groups of properties under a header, like 36 | // [SectionName]. The sectionHeader parser recognizes these headers. 37 | // - Empty lines: These are recognized by the emptyLine parser. They can appear 38 | // anywhere in the file and are ignored by the parser. 39 | // - Global Section: This is a special section that contains properties defined 40 | // before any named section. 41 | 42 | export class Property { 43 | constructor( 44 | public readonly name: string, 45 | public readonly value: string 46 | ) {} 47 | } 48 | 49 | export class EmptyLine {} 50 | 51 | export class NamedSection { 52 | constructor( 53 | public readonly name: string, 54 | public readonly properties: Property | EmptyLine[] 55 | ) {} 56 | } 57 | 58 | export class GlobalSection { 59 | constructor(public readonly properties: Property | EmptyLine[]) {} 60 | } 61 | 62 | export class IniFile { 63 | constructor( 64 | public readonly global: GlobalSection, 65 | public readonly sections: NamedSection[] 66 | ) {} 67 | } 68 | 69 | const maybeSpacesOrTabs = string(" ").pipe(or(string("\t")), many()); 70 | function token(parser: Parjser): Parjser { 71 | return parser.pipe(between(maybeSpacesOrTabs)); 72 | } 73 | 74 | const identifierCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; 75 | const valueCharacters = identifierCharacters + " \t"; 76 | 77 | export const comment = token(anyCharOf(";#")) 78 | .pipe(qthen(anyCharOf(valueCharacters).pipe(many(), stringify()))) 79 | .expects("comment"); 80 | 81 | export const identifier = token(anyCharOf(identifierCharacters).pipe(many1(), stringify())).expects( 82 | "identifier" 83 | ); 84 | export const value = anyCharOf(valueCharacters).pipe(many1(), stringify()).expects("value"); 85 | 86 | // parses a definition like `abc=def`. 87 | // Note that definitions are case insensitive in .ini files 88 | export const definitionLine = token(identifier) 89 | .pipe( 90 | thenq(token(string("="))), 91 | then(value, comment.pipe(maybe())), 92 | thenq(newline().pipe(or(eof()))), 93 | map(([name, val]) => new Property(name.toLowerCase(), val)) 94 | ) 95 | .expects("definition"); 96 | 97 | const emptyLine = comment 98 | .pipe(maybe(), qthen(maybeSpacesOrTabs)) 99 | .pipe(qthen(newline()), qthen(result(new EmptyLine()))) 100 | .expects("empty line"); 101 | 102 | // parses many definitions 103 | export const definitionList = emptyLine 104 | .pipe(or(definitionLine), many()) 105 | .expects("definitionSection"); 106 | 107 | export const sectionHeader = token( 108 | anyCharOf(valueCharacters).pipe(many1(), stringify(), between("[", "]")) 109 | ) 110 | .pipe(thenq(comment.pipe(maybe()))) 111 | .expects("section header"); 112 | 113 | export const section = sectionHeader 114 | .pipe( 115 | thenq(newline()), 116 | then(definitionList), 117 | map(([name, properties]) => new NamedSection(name, properties)) 118 | ) 119 | .expects("section"); 120 | 121 | // parses the variables at the top of the file. These are called global because they are not 122 | // associated with any section. 123 | export const globalSection = definitionList 124 | .pipe(map(properties => new GlobalSection(properties))) 125 | .expects("global section"); 126 | 127 | export const iniFile = globalSection 128 | .pipe( 129 | then(section.pipe(many())), 130 | map(([global, sections]) => new IniFile(global, sections)) 131 | ) 132 | .expects("ini file"); 133 | -------------------------------------------------------------------------------- /packages/parjs/src/internal/combinators/many-till.ts: -------------------------------------------------------------------------------- 1 | import type { ImplicitParjser, ParjsCombinator } from "../../index"; 2 | import type { CombinatorInput } from "../combinated"; 3 | import { Combinated } from "../combinated"; 4 | import { Issues } from "../issues"; 5 | import { ResultKind } from "../result"; 6 | import type { ParsingState, UserState } from "../state"; 7 | import { wrapImplicit } from "../wrap-implicit"; 8 | import { pipe } from "./combinator"; 9 | import { qthen } from "./then"; 10 | 11 | const defaultProjection = (sourceMatches: TSource[], till: unknown, state: unknown) => 12 | sourceMatches; 13 | 14 | class ManyTill extends Combinated { 15 | type = "manyTill"; 16 | expecting = `${this.source.expecting} or ${this._till.expecting}`; 17 | 18 | constructor( 19 | source: CombinatorInput, 20 | private readonly _till: CombinatorInput, 21 | private readonly _project: (source: TSource[], till: TTill, user: UserState) => TResult 22 | ) { 23 | super(source); 24 | } 25 | 26 | _apply(ps: ParsingState): void { 27 | let { position } = ps; 28 | const arr: TSource[] = []; 29 | let successes = 0; 30 | for (;;) { 31 | this._till.apply(ps); 32 | if (ps.isOk) { 33 | break; 34 | } else if (ps.atLeast(ResultKind.HardFail)) { 35 | // if till failed hard/fatally, we return the fail result. 36 | return; 37 | } 38 | // backtrack to before till failed. 39 | ps.position = position; 40 | this.source.apply(ps); 41 | if (ps.isOk) { 42 | arr.push(ps.value as TSource); 43 | } else if (ps.isSoft) { 44 | // many failed softly before till... 45 | ps.kind = successes === 0 ? ResultKind.SoftFail : ResultKind.HardFail; 46 | return; 47 | } else { 48 | // many failed hard/fatal 49 | return; 50 | } 51 | if (ps.position === position) { 52 | Issues.guardAgainstInfiniteLoop("manyTill"); 53 | } 54 | position = ps.position; 55 | successes++; 56 | } 57 | ps.value = this._project(arr, ps.value as TTill, ps.userState); 58 | ps.kind = ResultKind.Ok; 59 | } 60 | } 61 | 62 | /** 63 | * Tries to apply the source parser repeatedly until `till` succeeds. Yields the results of the 64 | * source parser in an array. 65 | * 66 | * @param till The parser that indicates iteration should stop. 67 | * @param pProject A projection to apply on the captured results. 68 | */ 69 | export function manyTill( 70 | till: ImplicitParjser, 71 | pProject: (source: TSource[], till: TTill, user: UserState) => TResult 72 | ): ParjsCombinator; 73 | export function manyTill( 74 | till: ImplicitParjser 75 | ): ParjsCombinator; 76 | export function manyTill( 77 | till: ImplicitParjser, 78 | pProject?: (source: TSource[], till: TTill, user: UserState) => TResult 79 | ): ParjsCombinator { 80 | const tillResolved = wrapImplicit(till); 81 | const project = pProject || defaultProjection; 82 | return (source: ImplicitParjser) => { 83 | return new ManyTill( 84 | wrapImplicit(source), 85 | tillResolved, 86 | project as any 87 | ); 88 | }; 89 | } 90 | 91 | /** 92 | * Applies `start` and then repeatedly applies the source parser until `pTill` succeeds. Similar to 93 | * a mix of `between` and `manyTill`. Yields the results of the source parser in an array. 94 | * 95 | * @param start The initial parser to apply. 96 | * @param pTill Optionally, the terminator. Defaults to `start`. 97 | * @param projection Optionally, a projection to apply on the captured results. 98 | */ 99 | export function manyBetween( 100 | start: ImplicitParjser, 101 | pTill?: ImplicitParjser, 102 | projection?: (sources: TSource[], till: TTill | TStart, state: UserState) => TResult 103 | ): ParjsCombinator { 104 | const till: ImplicitParjser = pTill || start; 105 | return source => { 106 | const wrapped = wrapImplicit(source); 107 | return pipe(start, qthen(wrapped.pipe(manyTill(till, projection as any)))); 108 | }; 109 | } 110 | --------------------------------------------------------------------------------