parent
85b36aa1b1
commit
721364abd4
@ -0,0 +1,5 @@ |
||||
<component name="ProjectCodeStyleConfiguration"> |
||||
<state> |
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" /> |
||||
</state> |
||||
</component> |
@ -0,0 +1,53 @@ |
||||
'use strict'; |
||||
|
||||
const babelJest = require('babel-jest'); |
||||
const makeCacheKey = require('@jest/create-cache-key-function').default(); |
||||
const peggy = require("peggy"); |
||||
|
||||
const hasJsxRuntime = (() => { |
||||
if (process.env.DISABLE_NEW_JSX_TRANSFORM === 'true') { |
||||
return false; |
||||
} |
||||
|
||||
try { |
||||
require.resolve('react/jsx-runtime'); |
||||
return true; |
||||
} catch (e) { |
||||
return false; |
||||
} |
||||
})(); |
||||
|
||||
const babelJestInstance = babelJest.createTransformer({ |
||||
presets: [ |
||||
[ |
||||
require.resolve('babel-preset-react-app'), |
||||
{ |
||||
runtime: hasJsxRuntime ? 'automatic' : 'classic', |
||||
}, |
||||
], |
||||
], |
||||
babelrc: false, |
||||
configFile: false, |
||||
}); |
||||
|
||||
const startRuleFinder = /\s*\/\/\s*peggy-loader:startRule\s*(\S+)\s*\n/g |
||||
|
||||
module.exports = { |
||||
canInstrument: babelJestInstance.canInstrument, |
||||
createTransformer() { |
||||
return this; |
||||
}, |
||||
getCacheKey(sourceText, sourcePath, configString, options) { |
||||
return makeCacheKey(sourceText, sourcePath, configString, options) + babelJestInstance.getCacheKey(sourceText, sourcePath, configString, options) |
||||
}, |
||||
process(sourceText, sourcePath, configString, options) { |
||||
const start = Array.from({ [Symbol.iterator]() { return sourceText.matchAll(startRuleFinder) } }, ([_, rule]) => rule) |
||||
const resultSource = peggy.generate(sourceText, { |
||||
grammarSource: this.resourcePath, |
||||
allowedStartRules: start, |
||||
output: "source", |
||||
format: "es", |
||||
}) |
||||
return babelJest.process(resultSource, sourcePath, configString, options) |
||||
}, |
||||
} |
@ -0,0 +1,37 @@ |
||||
const peggy = require("peggy") |
||||
const loaderUtils = require("loader-utils") |
||||
|
||||
const startRuleFinder = /\s*\/\/\s*peggy-loader:startRule\s*(\S+)\s*\n/g |
||||
|
||||
async function loadPeggy(rawSource) { |
||||
const callback = this.async(); |
||||
const tracks = await import("peggy-tracks") |
||||
const rawOpts = loaderUtils.getOptions(this) |
||||
const opts = typeof rawOpts === "object" && rawOpts !== null ? rawOpts : {} |
||||
const peggyOpts = opts.hasOwnProperty("peggy") && typeof opts.peggy === "object" && opts.peggy !== null ? opts.peggy : {} |
||||
const plugins = peggyOpts.hasOwnProperty("plugins") && Array.isArray(peggyOpts.plugins) ? peggyOpts.plugins : [] |
||||
const tracksOpts = opts.hasOwnProperty("tracks") && typeof opts.tracks === "object" && opts.tracks !== null ? opts.tracks : {start: []} |
||||
const start = Array.from({ [Symbol.iterator]() { return rawSource.matchAll(startRuleFinder) } }, ([_, rule]) => rule) |
||||
const trackStarts = !opts.hasOwnProperty("tracks") ? [] : typeof tracksOpts.start === "string" ? [tracksOpts.start] : Array.isArray(tracksOpts.start) ? tracksOpts.start : start.length > 0 ? start : [undefined] |
||||
const source = rawSource.replaceAll(startRuleFinder, "\n") |
||||
const resultSource = peggy.generate(source, { |
||||
...peggyOpts, |
||||
grammarSource: this.resourcePath, |
||||
allowedStartRules: peggyOpts.allowedStartRules || start, |
||||
output: "source", |
||||
format: "es", |
||||
plugins, |
||||
}) |
||||
for (const start of trackStarts) { |
||||
const diagram = tracks.tracks({ |
||||
...tracksOpts, |
||||
text: source, |
||||
start, |
||||
}) |
||||
this.emitFile(`/tracks/${this.resourcePath}${typeof start === "string" ? "." + start : ""}.svg`, diagram.toStandalone(tracks.defaultCSS)) |
||||
} |
||||
// const fixedSource = resultSource.replace("export class SyntaxError", "class SyntaxError")
|
||||
callback(undefined, resultSource) |
||||
} |
||||
|
||||
module.exports = loadPeggy |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,30 @@ |
||||
{{ |
||||
// global (one-time) initializer |
||||
import { scriptFile } from "./ScriptFile"; |
||||
import { numberValue } from "./ScriptValue"; |
||||
import { versionStatement } from "./TopLevelStatement"; |
||||
}} |
||||
|
||||
{ |
||||
// per-parse initializer |
||||
} |
||||
|
||||
// peggy-loader:startRule ScriptFile |
||||
ScriptFile = statements:TopLevelStatementList { return scriptFile(statements); } |
||||
|
||||
NewLine "newline" = "\n" { return null; } |
||||
WhiteSpace "whitespace" = [ \r\v\t]+ { return null; } |
||||
LineComment "line comment" = "//" [^\n]* { return null; } |
||||
BlockCommentTail = "*/" / ( "*" !"/" [^*]* BlockCommentTail ) { return null; } |
||||
BlockComment "block comment" = "/*" [^*]* BlockCommentTail { return null; } |
||||
|
||||
IntegerLiteral "integer literal" = digits:$[0-9]+ { return numberValue(digits); } |
||||
|
||||
NonToken = (LineComment / BlockComment / WhiteSpace) { return null; } |
||||
_ = (NonToken+) { return null; } |
||||
StatementEnd = _? NewLine { return null; } |
||||
|
||||
VersionStatement "version statement" = "script" _ "version" _ version:IntegerLiteral { return versionStatement(version.value); } |
||||
|
||||
TopLevelStatement "top-level statement" = VersionStatement |
||||
TopLevelStatementList = (StatementEnd* _? head:TopLevelStatement tail:(StatementEnd+ _? @TopLevelStatement)* StatementEnd* _? { return [head, ...tail]; }) / (StatementEnd* _? { return []; }) |
@ -0,0 +1,34 @@ |
||||
import {ScriptFile} from "./ScriptValue.js" |
||||
|
||||
export interface Options { |
||||
/** Whether this is a script or an expression. Required. */ |
||||
readonly start: "ScriptFile"|"Expression" |
||||
/** The file (or other source) the text is being parsed from. Required.*/ |
||||
readonly grammarSource: string |
||||
} |
||||
|
||||
export interface Position { |
||||
readonly offset: number |
||||
readonly line: number |
||||
readonly column: number |
||||
} |
||||
|
||||
export interface Location { |
||||
readonly source: string |
||||
readonly start: Position |
||||
readonly end: Position |
||||
} |
||||
|
||||
// For later:
|
||||
//export function parse(text: string, options: Options & {readonly start: "Expression"}): Expression
|
||||
export function parse(text: string, options: Options & {readonly start: "ScriptFile"}): ScriptFile |
||||
|
||||
export class SyntaxError extends Error { |
||||
readonly name: "SyntaxError" |
||||
readonly message: string |
||||
readonly expected: (readonly string[])|null |
||||
readonly found: string|null |
||||
readonly location: Location |
||||
constructor(message: string, expected: (readonly string[])|null, found: string|null, location: Location) |
||||
format(mappings: readonly {readonly source: string, readonly text: string}[]): string |
||||
} |
@ -0,0 +1,83 @@ |
||||
import { parse, SyntaxError } from "./NomScript.peggy"; |
||||
|
||||
describe("parse", () => { |
||||
describe("ScriptFile", () => { |
||||
test("works with an empty input", () => { |
||||
expect(() => parse("", {grammarSource: "testData", start: "ScriptFile"})).not.toThrow() |
||||
}); |
||||
test("works with empty lines", () => { |
||||
expect(() => parse("\n\n\n\n\n", {grammarSource: "testData", start: "ScriptFile"})).not.toThrow() |
||||
}); |
||||
test("works with lines filled only with whitespace", () => { |
||||
expect(() => parse("\n\n \n\t\t \t\n\n", {grammarSource: "testData", start: "ScriptFile"})).not.toThrow() |
||||
}); |
||||
test("works with lines filled only with comments", () => { |
||||
expect(() => parse("\n\n /* */ \n// test \n /* */\n", {grammarSource: "testData", start: "ScriptFile"})).not.toThrow() |
||||
}); |
||||
test("throws SyntaxError for invalid text", () => { |
||||
expect(() => parse("!! invalid !!", {grammarSource: "testData", start: "ScriptFile"})).toThrow(SyntaxError) |
||||
}); |
||||
}) |
||||
}); |
||||
|
||||
describe("SyntaxError", () => { |
||||
describe("constructor", () => { |
||||
test("populates the members accordingly", () => { |
||||
const location = { |
||||
source: "source", |
||||
start: { |
||||
offset: 0, |
||||
line: 1, |
||||
column: 1, |
||||
}, |
||||
end: { |
||||
offset: 7, |
||||
line: 1, |
||||
column: 8, |
||||
}, |
||||
}; |
||||
const err = new SyntaxError("message", ["expected"], "found", location); |
||||
|
||||
expect(err.name).toEqual("SyntaxError"); |
||||
expect(err.message).toEqual("message"); |
||||
expect(err.expected).toEqual(["expected"]); |
||||
expect(err.found).toEqual("found"); |
||||
expect(err.location).toEqual(location); |
||||
}); |
||||
}); |
||||
describe("format", () => { |
||||
test("displays a pretty error message for the failure", () => { |
||||
const location = { |
||||
source: "source", |
||||
start: { |
||||
offset: 0, |
||||
line: 1, |
||||
column: 1, |
||||
}, |
||||
end: { |
||||
offset: 7, |
||||
line: 1, |
||||
column: 8, |
||||
}, |
||||
}; |
||||
const err = new SyntaxError( |
||||
"message", |
||||
["valid", "cool"], |
||||
"errored", |
||||
location |
||||
); |
||||
const result = err.format([ |
||||
{ source: "source", text: "errored text" }, |
||||
{ source: "source2", text: "irrelevant" }, |
||||
]); |
||||
|
||||
expect(result).toMatchInlineSnapshot(` |
||||
"Error: message |
||||
--> source:1:1 |
||||
| |
||||
1 | errored text |
||||
| ^^^^^^^" |
||||
`);
|
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,10 @@ |
||||
import {TopLevelStatement} from "./TopLevelStatement.js"; |
||||
|
||||
export interface ScriptFile { |
||||
readonly statements: readonly TopLevelStatement[] |
||||
} |
||||
export function scriptFile(statements: readonly TopLevelStatement[]): ScriptFile { |
||||
return { |
||||
statements |
||||
} |
||||
} |
@ -0,0 +1,29 @@ |
||||
import {isNumberValue, numberValue, ScriptValueTypes} from "./ScriptValue"; |
||||
|
||||
describe("NumberValue", () => { |
||||
describe("constructor", () => { |
||||
test("forwards number parameter to the value property", () => { |
||||
expect(numberValue(5)).toEqual({ |
||||
type: ScriptValueTypes.NUMBER, |
||||
value: 5 |
||||
}) |
||||
}) |
||||
test("parses string parameter to the value property", () => { |
||||
expect(numberValue("99")).toEqual({ |
||||
type: ScriptValueTypes.NUMBER, |
||||
value: 99 |
||||
}) |
||||
}) |
||||
}) |
||||
describe("typecheck", () => { |
||||
test("passes on the output of the constructor", () => { |
||||
expect(isNumberValue(numberValue(2))).toBeTruthy() |
||||
}) |
||||
test("passes on a hand-constructed instance", () => { |
||||
expect(isNumberValue({ |
||||
type: ScriptValueTypes.NUMBER, |
||||
value: 21 |
||||
})).toBeTruthy() |
||||
}) |
||||
}) |
||||
}) |
@ -0,0 +1,26 @@ |
||||
export enum ScriptValueTypes { |
||||
NUMBER = "number" |
||||
} |
||||
|
||||
export interface NumberValue { |
||||
readonly type: ScriptValueTypes.NUMBER, |
||||
readonly value: number, |
||||
} |
||||
export function numberValue(value: number|string) { |
||||
if (typeof value === "string") { |
||||
return { |
||||
type: ScriptValueTypes.NUMBER, |
||||
value: parseInt(value, 10), |
||||
} |
||||
} else { |
||||
return { |
||||
type: ScriptValueTypes.NUMBER, |
||||
value, |
||||
} |
||||
} |
||||
} |
||||
export function isNumberValue(value: ScriptValue): value is NumberValue { |
||||
return value.type === ScriptValueTypes.NUMBER |
||||
} |
||||
|
||||
export type ScriptValue = NumberValue |
@ -0,0 +1,59 @@ |
||||
import {parse as parseInternal, SyntaxError} from "./NomScript.peggy" |
||||
import {isVersionStatement, TopLevelStatementTypes, VersionStatement, versionStatement} from "./TopLevelStatement"; |
||||
import {scriptFile, ScriptFile} from "./ScriptFile"; |
||||
|
||||
function parse(text: string): ScriptFile { |
||||
return parseInternal(text, {start: "ScriptFile", grammarSource: "testData"}) |
||||
} |
||||
|
||||
describe("VersionStatement", () => { |
||||
describe("constructor", () => { |
||||
test("forwards parameter to the version property", () => { |
||||
expect(versionStatement(5)).toEqual({ |
||||
type: TopLevelStatementTypes.VERSION, |
||||
version: 5 |
||||
}) |
||||
}) |
||||
}) |
||||
describe("typecheck", () => { |
||||
test("passes on the output of the constructor", () => { |
||||
expect(isVersionStatement(versionStatement(2))).toBeTruthy() |
||||
}) |
||||
test("passes on a hand-constructed instance", () => { |
||||
expect(isVersionStatement({ |
||||
type: TopLevelStatementTypes.VERSION, |
||||
version: 5 |
||||
})).toBeTruthy() |
||||
}) |
||||
}) |
||||
describe("parsing", () => { |
||||
function success(name: string, text: string, ...result: VersionStatement[]) { |
||||
test(`succeeds for ${name}`, () => { |
||||
expect(parse(text)).toEqual(scriptFile(result)) |
||||
}) |
||||
} |
||||
|
||||
success("basic example", "script version 1", versionStatement(1)) |
||||
success("followed by line comment", "script version 3 // and nine tenths", versionStatement(3)) |
||||
success("preceded by block comment", "/* just because it's a */ script version 2", versionStatement(2)) |
||||
success("preceded by line comment", "// line comment\nscript version 9999", versionStatement(9999)) |
||||
success("preceded by spaces", "\n\n\n\t\t\tscript version 2", versionStatement(2)) |
||||
success("separated by tabs instead of spaces", "script\tversion\t4", versionStatement(4)) |
||||
success("separated by comments instead of spaces", "script/* or candy */version/* if it can be called that */4", versionStatement(4)) |
||||
success("separated by multiline comments instead of spaces", "script/* ripped\n */version/* mergin\n */4", versionStatement(4)) |
||||
success("doubled on separate lines", "script version 1\nscript version 2", versionStatement(1), versionStatement(2)) |
||||
|
||||
function failure(name: string, text: string) { |
||||
test(`fails for ${name}`, () => { |
||||
expect(() => parse(text)).toThrow(SyntaxError) |
||||
}) |
||||
} |
||||
|
||||
failure("separated by newlines", "script\nversion\n1") |
||||
failure("not separated", "scriptversion1") |
||||
failure("wrong order", "version script 1") |
||||
failure("decimal version", "version script 1.5") |
||||
failure("followed by garbage", "script version 1 and a half") |
||||
failure("doubled on same line", "script version 1 script version 1") |
||||
}) |
||||
}) |
@ -0,0 +1,19 @@ |
||||
export enum TopLevelStatementTypes { |
||||
VERSION = "version", |
||||
} |
||||
|
||||
export interface VersionStatement { |
||||
readonly type: TopLevelStatementTypes.VERSION, |
||||
readonly version: number, |
||||
} |
||||
export function versionStatement(version: number): VersionStatement { |
||||
return { |
||||
type: TopLevelStatementTypes.VERSION, |
||||
version, |
||||
} |
||||
} |
||||
export function isVersionStatement(statement: TopLevelStatement): statement is VersionStatement { |
||||
return statement.type === TopLevelStatementTypes.VERSION |
||||
} |
||||
|
||||
export type TopLevelStatement = VersionStatement |
Loading…
Reference in new issue