Skip to content

Add source-with-inline-map #282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jun 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,27 @@ Released: TBD

### Major Changes

- None
- [#280](https://github.com/peggyjs/peggy/issues/280) Add inline examples to
the documentation, from @hildjj

### Minor Changes

- [TBD] Use commander's new `.conflicts()` to check for mutually-exclusive CLI
options, from @hildjj
- [TBD] `"*"` is now a valid `allowedStartRule`, which means all rules are allowed, from @hildjj
- [#274](https://github.com/peggyjs/peggy/issues/274) Use commander's new
`.conflicts()` to check for mutually-exclusive CLI options, from @hildjj
- [#274](https://github.com/peggyjs/peggy/issues/274) `"*"` is now a valid `allowedStartRule`, which means all rules are allowed, from @hildjj
- [#229](https://github.com/peggyjs/peggy/issues/229) new CLI option
`-S <rule>` or `--start-rule <rule>` to specify the start rule when testing,
from @hildjj
- [#236](https://github.com/peggyjs/peggy/issues/236) Website: show line numbers
in parser input textarea, from @Mingun
- [#280](https://github.com/peggyjs/peggy/issues/280) new output type
`source-with-inline-map`, which generates source text with an inline map,
from @hildjj

### Bug Fixes

- None
- [#283](https://github.com/peggyjs/peggy/issues/283) Fix incorrect type
information for DiagnosticCallback, from @hildjj

2.0.1
-----
Expand Down
52 changes: 37 additions & 15 deletions docs/documentation.html
Original file line number Diff line number Diff line change
Expand Up @@ -376,10 +376,13 @@ <h3 id="generating-a-parser-javascript-api">JavaScript API</h3>
<code>null</code>).</dd>

<dt><code>format</code></dt>
<dd>format of the generated parser (<code>"amd"</code>, <code>"bare"</code>,
<code>"commonjs"</code>, <code>"es"</code>, <code>"globals"</code>, or
<code>"umd"</code>); valid only when <code>output</code> is set to
<code>"source"</code> (default: <code>"bare"</code>).</dd>
<dd>
Format of the generated parser (<code>"amd"</code>, <code>"bare"</code>,
<code>"commonjs"</code>, <code>"es"</code>, <code>"globals"</code>, or
<code>"umd"</code>); valid only when <code>output</code> is set to
<code>"source"</code>, <code>"source-and-map"</code>, or
<code>"source-with-inline-map"</code>. (default: <code>"bare"</code>).
</dd>

<dt><code>grammarSource</code></dt>
<dd>any object that represent origin of the input grammar. The CLI will set
Expand All @@ -392,17 +395,36 @@ <h3 id="generating-a-parser-javascript-api">JavaScript API</h3>
<dd>Callback for informational messages. See <a href="#error-reporting">Error Reporting</a></dd>

<dt><code>output</code></dt>
<dd><p>If set to <code>"parser"</code>, the method will return generated parser
object; if set to <code>"source"</code>, it will return parser source code as
a string (default: <code>"parser"</code>).
If set to <code>"source-and-map"</code>, it will return a <a href="https://github.com/mozilla/source-map#sourcenode"><code>SourceNode</code></a> object; you can
get source code by calling <code>toString()</code> method or source code and mapping by
calling <code>toStringWithSourceMap()</code> method, see the <a href="https://github.com/mozilla/source-map#sourcenode"><code>SourceNode</code></a> documentation
(default: <code>"parser"</code>)</p>
<blockquote>
<p><strong>Note</strong>: because of bug <a href="https://github.com/mozilla/source-map/issues/444">source-map/444</a> you should also set <code>grammarSource</code> to
a not-empty string if you set this value to <code>"source-and-map"</code></p>
</blockquote></dd>
<dd><p>A string, one of:</p>
<ul>
<li><code>"parser"</code> - return generated parser object.</li>
<li><code>"source"</code> - return parser source code as a string.</li>
<li><code>"source-and-map"</code> - return a
<a href="https://github.com/mozilla/source-map#sourcenode"><code>SourceNode</code></a>
object; you can get source code by calling <code>toString()</code>
method or source code and mapping by calling
<code>toStringWithSourceMap()</code> method, see the
<a href="https://github.com/mozilla/source-map#sourcenode"><code>SourceNode</code></a>
documentation.
</li>
<li><code>"source-with-inline-map"</code> - return the parser source along
with an embedded source map as a <code>data:</code> URI. This option
leads to a larger output string, but is the easiest to integrate with
developer tooling.</li>
</ul>
<p>(default: <code>"parser"</code>)</p>
<blockquote>
<p><strong>Note</strong>: because of bug <a
href="https://github.com/mozilla/source-map/issues/444">source-map/444</a>
you should also set <code>grammarSource</code> to a not-empty string if
you set this value to <code>"source-and-map"</code> or
<code>"source-with-inline-map"</code>. The path should be relative to
the location where the generated parser code will be stored. For
example, if you are generating <code>lib/parser.js</code> from
<code>src/parser.peggy</code>, then your options should be:
<code>{ grammarSource: "../src/parser.peggy" }</code></p>
</blockquote>
</dd>

<dt><code>plugins</code></dt>
<dd>Plugins to use. See the <a href="#plugins-api">Plugins API</a> section.</dd>
Expand Down
2 changes: 1 addition & 1 deletion docs/js/benchmark-bundle.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/js/test-bundle.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/vendor/peggy/peggy.min.js

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions lib/compiler/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const reportUndefinedRules = require("./passes/report-undefined-rules");
const reportIncorrectPlucking = require("./passes/report-incorrect-plucking");
const Session = require("./session");
const visitor = require("./visitor");
const { base64 } = require("./utils");

function processOptions(options, defaults) {
const processedOptions = {};
Expand Down Expand Up @@ -118,6 +119,20 @@ const compiler = {
case "source-and-map":
return ast.code;

case "source-with-inline-map": {
if (typeof TextEncoder === "undefined") {
throw new Error("TextEncoder is not supported by this platform");
}
const sourceMap = ast.code.toStringWithSourceMap();
const encoder = new TextEncoder();
const b64 = base64(
encoder.encode(JSON.stringify(sourceMap.map.toJSON()))
);
return sourceMap.code + `\
//\x23 sourceMappingURL=data:application/json;charset=utf-8;base64,${b64}
`;
}

default:
throw new Error("Invalid output format: " + options.output + ".");
}
Expand Down
39 changes: 39 additions & 0 deletions lib/compiler/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,42 @@ function regexpClassEscape(s) {
.replace(/[\u1000-\uFFFF]/g, ch => "\\u" + hex(ch));
}
exports.regexpClassEscape = regexpClassEscape;

/**
* Base64 encode a Uint8Array. Needed for browser compatibility where
* the Buffer class is not available.
*
* @param {Uint8Array} u8 Bytes to encode
* @returns {string} Base64 encoded string
*/
function base64(u8) {
// Note: btoa has the worst API, and even mentioning Buffer here will
// cause rollup to suck it in.

// See RFC4648, sec. 4.
// https://datatracker.ietf.org/doc/html/rfc4648#section-4
const A = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const rem = u8.length % 3;
const len = u8.length - rem;
let res = "";

for (let i = 0; i < len; i += 3) {
res += A[u8[i] >> 2];
res += A[((u8[i] & 0x3) << 4) | (u8[i + 1] >> 4)];
res += A[((u8[i + 1] & 0xf) << 2) | (u8[i + 2] >> 6)];
res += A[u8[i + 2] & 0x3f];
}
if (rem === 1) {
res += A[u8[len] >> 2];
res += A[(u8[len] & 0x3) << 4];
res += "==";
} else if (rem === 2) {
res += A[u8[len] >> 2];
res += A[((u8[len] & 0x3) << 4) | (u8[len + 1] >> 4)];
res += A[(u8[len + 1] & 0xf) << 2];
res += "=";
}

return res;
}
exports.base64 = base64;
100 changes: 53 additions & 47 deletions lib/peg.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,12 @@ export namespace compiler {
options: SourceBuildOptions<"source">
): string;

function compile(
ast: ast.Grammar,
stages: Stages,
options: SourceBuildOptions<"source-with-inline-map">
): string;

/**
* Generates a parser source and source map from a specified grammar AST.
*
Expand Down Expand Up @@ -991,50 +997,20 @@ export interface Session {
): void;
}

export interface DiagnosticCallback {
/**
* Called when compiler reports an error.
*
* @param stage Stage in which this diagnostic was originated
* @param message Main message, which should describe error objectives
* @param location If defined, this is location described in the `message`
* @param notes Additional messages with context information
*/
error?(
stage: Stage,
message: string,
location?: LocationRange,
notes?: DiagnosticNote[]
): void;
/**
* Called when compiler reports a warning.
*
* @param stage Stage in which this diagnostic was originated
* @param message Main message, which should describe warning objectives
* @param location If defined, this is location described in the `message`
* @param notes Additional messages with context information
*/
warning?(
stage: Stage,
message: string,
location?: LocationRange,
notes?: DiagnosticNote[]
): void;
/**
* Called when compiler reports an informational message.
*
* @param stage Stage in which this diagnostic was originated
* @param message Main message, which gives information about an event
* @param location If defined, this is location described in the `message`
* @param notes Additional messages with context information
*/
info?(
stage: Stage,
message: string,
location?: LocationRange,
notes?: DiagnosticNote[]
): void;
}
/**
* Called when compiler reports an error, warning, or info.
*
* @param stage Stage in which this diagnostic was originated
* @param message Main message, which should describe error objectives
* @param location If defined, this is location described in the `message`
* @param notes Additional messages with context information
*/
export type DiagnosticCallback = (
stage: Stage,
message: string,
location?: LocationRange,
notes?: DiagnosticNote[]
) => void;

/**
* Parser dependencies, is an object which maps variables used to access the
Expand Down Expand Up @@ -1087,14 +1063,20 @@ export interface ParserBuildOptions extends BuildOptionsBase {
* If set to `"parser"`, the method will return generated parser object;
* if set to `"source"`, it will return parser source code as a string;
* if set to `"source-and-map"`, it will return a `SourceNode` object
* which can give a parser source code as a string and a source map;
* which can give a parser source code as a string and a source map;
* if set to `"source-with-inline-map"`, it will return the parser source
* along with an embedded source map as a `data:` URI;
* (default: `"parser"`)
*/
output?: "parser";
}

/** Possible kinds of source output generators. */
export type SourceOutputs = "source" | "source-and-map";
export type SourceOutputs =
"parser" |
"source" |
"source-and-map" |
"source-with-inline-map";

/** Base options for all source-generating formats. */
interface SourceOptionsBase<Output extends SourceOutputs>
Expand All @@ -1103,7 +1085,9 @@ interface SourceOptionsBase<Output extends SourceOutputs>
* If set to `"parser"`, the method will return generated parser object;
* if set to `"source"`, it will return parser source code as a string;
* if set to `"source-and-map"`, it will return a `SourceNode` object
* which can give a parser source code as a string and a source map;
* which can give a parser source code as a string and a source map;
* if set to `"source-with-inline-map"`, it will return the parser source
* along with an embedded source map as a `data:` URI;
* (default: `"parser"`)
*/
output: Output;
Expand Down Expand Up @@ -1197,6 +1181,28 @@ export function generate(
options: SourceBuildOptions<"source">
): string;

/**
* Returns the generated source code as a string appended with a source map as
* a `data:` URI.
*
* Note, `options.grammarSource` MUST contain a string that is a path relative
* to the location the generated source will be written to, as no further path
* processing will be performed.
*
* @param grammar String in the format described by the meta-grammar in the
* `parser.pegjs` file
* @param options Options that allow you to customize returned parser object
*
* @throws {SyntaxError} If the grammar contains a syntax error, for example,
* an unclosed brace
* @throws {GrammarError} If the grammar contains a semantic error, for
* example, duplicated labels
*/
export function generate(
grammar: string,
options: SourceBuildOptions<"source-with-inline-map">
): string;

/**
* Returns the generated source code and its source map as a `SourceNode`
* object. You can get the generated code and the source map by using a
Expand Down
53 changes: 51 additions & 2 deletions test/types/peg.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,35 @@ const src = readFileSync(
"utf8"
);

const problems: peggy.Problem[] = [];

function error(
stage: peggy.Stage,
message: string,
location?: peggy.LocationRange,
notes?: peggy.DiagnosticNote[]
): void {
problems.push(["error", message, location, notes]);
}

function info(
stage: peggy.Stage,
message: string,
location?: peggy.LocationRange,
notes?: peggy.DiagnosticNote[]
): void {
problems.push(["info", message, location, notes]);
}

function warning(
stage: peggy.Stage,
message: string,
location?: peggy.LocationRange,
notes?: peggy.DiagnosticNote[]
): void {
problems.push(["warning", message, location, notes]);
}

describe("peg.d.ts", () => {
it("executes a grammar", () => {
expectType<string>(src);
Expand Down Expand Up @@ -45,7 +74,12 @@ describe("peg.d.ts", () => {
});

it("takes a valid tracer", () => {
const parser = peggy.generate(src, { trace: true });
const parser = peggy.generate(src, {
trace: true,
error,
info,
warning,
});
expectType<peggy.Parser>(parser);

parser.parse(" /**/ 1\n", {
Expand Down Expand Up @@ -81,6 +115,9 @@ describe("peg.d.ts", () => {

const p3 = peggy.generate(src, { output: true as boolean ? "source-and-map" : "source" });
expectType<string | SourceNode>(p3);

const p4 = peggy.generate(src, { output: "source-with-inline-map" });
expectType<string>(p4);
});

it("compiles with source map", () => {
Expand All @@ -107,6 +144,13 @@ describe("peg.d.ts", () => {
{ output: true as boolean ? "source-and-map" : "source" }
);
expectType<string | SourceNode>(p3);

const p4 = peggy.compiler.compile(
ast,
peggy.compiler.passes,
{ output: "source-with-inline-map" }
);
expectType<string>(p4);
});

it("creates an AST", () => {
Expand Down Expand Up @@ -369,7 +413,12 @@ describe("peg.d.ts", () => {
expectType<peggy.ast.Grammar>(ast);
const parser = peggy.compiler.compile(
ast,
peggy.compiler.passes
peggy.compiler.passes,
{
error,
info,
warning,
}
);
expectType<peggy.Parser>(parser);
expectType<peggy.ast.MatchResult | undefined>(ast.rules[0].match);
Expand Down
Loading