From 68eb074ef1e1579212a86aea33f67d7f78a07a3c Mon Sep 17 00:00:00 2001 From: Patrick Smith Date: Mon, 27 Nov 2023 20:41:55 +1100 Subject: [PATCH] New type system, and Deno! Still in progress --- .vscode/settings.json | 3 + README.md | 71 ++++--- deno.json | 8 + deno.lock | 17 ++ examples/deno.ts | 12 +- jest.config.js | 4 +- src/index.test.ts | 387 +++++++++++++++++++------------------- src/index.ts | 122 ++++++++---- src/math.test.ts | 65 ++++--- src/media-query.test.ts | 302 ++++++++++++++++++++++------- src/modules.test.ts | 243 ++++++++++++------------ src/natural-dates.test.ts | 236 ++++++++++++++++------- src/routing.test.ts | 22 ++- src/tailwindcss.test.ts | 201 ++++++++++---------- src/test-deps.ts | 7 + tsconfig.json | 1 + 16 files changed, 1051 insertions(+), 650 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 deno.json create mode 100644 deno.lock create mode 100644 src/test-deps.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..974a2b4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": false +} diff --git a/README.md b/README.md index fd9f787..b7ba737 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,27 @@ npm add yieldparser ## Overview -Yieldparser parses a source chunk-by-chunk. You define a generator function that yields each chunk to be found. This chunk can be a `string`, a `RexExp`, or another generator function. Your generator function receives replies from parsing that chunk, for example a regular expression would receive a reply with the matches that were found. You then use this information to build a result: the value that your generator function returns. This could be a simple value, or it could be an entire AST (abstract syntax tree). +Yieldparser parses a source chunk-by-chunk. You define a generator function that +yields each chunk to be found. This chunk can be a `string`, a `RexExp`, or +another generator function. Your generator function receives replies from +parsing that chunk, for example a regular expression would receive a reply with +the matches that were found. You then use this information to build a result: +the value that your generator function returns. This could be a simple value, or +it could be an entire AST (abstract syntax tree). -If you yield an array of choices, then each choice is tested and the first one that matches is used. +If you yield an array of choices, then each choice is tested and the first one +that matches is used. -If your chunks don’t match the input string, then an error result is returned with the remaining string and the chunk that it failed on. If it succeeds, then a success result is returned with the return value of the generator function, and the remaining string (if there is anything remaining). +If your chunks don’t match the input string, then an error result is returned +with the remaining string and the chunk that it failed on. If it succeeds, then +a success result is returned with the return value of the generator function, +and the remaining string (if there is anything remaining). -Run `parse(input, yourGeneratorIterable)` to take an input string and parse into a result. +Run `parse(input, yourGeneratorIterable)` to take an input string and parse into +a result. -Run `invert(output, yourGeneratorIterable)` to take an expected result and map it back to a source string. +Run `invert(output, yourGeneratorIterable)` to take an expected result and map +it back to a source string. ## Examples @@ -41,12 +53,13 @@ Run `invert(output, yourGeneratorIterable)` to take an expected result and map i ### Routes parser -Define a generator function for each route you have, and then define a top level `Routes` generator function. Then parse your path using `parse()`. +Define a generator function for each route you have, and then define a top level +`Routes` generator function. Then parse your path using `parse()`. You can also map from a route object back to a path string using `invert()`. ```typescript -import { parse, mustEnd, invert } from "yieldparser"; +import { invert, mustEnd, parse } from "yieldparser"; type Route = | { type: "home" } @@ -99,23 +112,23 @@ function* Routes() { return yield [Home, About, Terms, BlogRoutes]; } -parse("/", Routes()) // result: { type: "home" }, success: true, remaining: "" } -parse("/about", Routes()) // result: { type: "about" }, success: true, remaining: "" } -parse("/legal/terms", Routes()) // result: { type: "terms" }, success: true, remaining: "" } -parse("/blog", Routes()) // result: { type: "blog" }, success: true, remaining: "" } -parse("/blog/happy-new-year", Routes()) // result: { type: "blogArticle", slug: "happy-new-year" }, success: true, remaining: "" } - -invert({ type: "home" }, Routes()) // "/" -invert({ type: "about" }, Routes()) // "/about" -invert({ type: "terms" }, Routes()) // "/legal/terms" -invert({ type: "blog" }, Routes()) // "/blog" -invert({ type: "blogArticle", slug: "happy-new-year" }, Routes()) // "/blog/happy-new-year" +parse("/", Routes()); // result: { type: "home" }, success: true, remaining: "" } +parse("/about", Routes()); // result: { type: "about" }, success: true, remaining: "" } +parse("/legal/terms", Routes()); // result: { type: "terms" }, success: true, remaining: "" } +parse("/blog", Routes()); // result: { type: "blog" }, success: true, remaining: "" } +parse("/blog/happy-new-year", Routes()); // result: { type: "blogArticle", slug: "happy-new-year" }, success: true, remaining: "" } + +invert({ type: "home" }, Routes()); // "/" +invert({ type: "about" }, Routes()); // "/about" +invert({ type: "terms" }, Routes()); // "/legal/terms" +invert({ type: "blog" }, Routes()); // "/blog" +invert({ type: "blogArticle", slug: "happy-new-year" }, Routes()); // "/blog/happy-new-year" ``` ### IP Address parser ```typescript -import { parse, mustEnd } from 'yieldparser'; +import { mustEnd, parse } from "yieldparser"; function* Digit() { const [digit]: [string] = yield /^\d+/; @@ -128,17 +141,17 @@ function* Digit() { function* IPAddress() { const first = yield Digit; - yield '.'; + yield "."; const second = yield Digit; - yield '.'; + yield "."; const third = yield Digit; - yield '.'; + yield "."; const fourth = yield Digit; yield mustEnd; return [first, second, third, fourth]; } -parse('1.2.3.4', IPAddress()); +parse("1.2.3.4", IPAddress()); /* { success: true, @@ -147,7 +160,7 @@ parse('1.2.3.4', IPAddress()); } */ -parse('1.2.3.256', IPAddress()); +parse("1.2.3.256", IPAddress()); /* { success: false, @@ -166,7 +179,7 @@ parse('1.2.3.256', IPAddress()); ### Basic CSS parser ```typescript -import { parse, hasMore, has } from 'yieldparser'; +import { has, hasMore, parse } from "yieldparser"; type Selector = string; interface Declaraction { @@ -193,11 +206,11 @@ function* ValueParser() { function* DeclarationParser() { const name = yield PropertyParser; yield whitespaceMay; - yield ':'; + yield ":"; yield whitespaceMay; const rawValue = yield ValueParser; yield whitespaceMay; - yield ';'; + yield ";"; return { name, rawValue }; } @@ -207,9 +220,9 @@ function* RuleParser() { const [selector]: [string] = yield /(:root|[*]|[a-z][\w]*)/; yield whitespaceMay; - yield '{'; + yield "{"; yield whitespaceMay; - while ((yield has('}')) === false) { + while ((yield has("}")) === false) { yield whitespaceMay; declarations.push(yield DeclarationParser); yield whitespaceMay; diff --git a/deno.json b/deno.json new file mode 100644 index 0000000..a2124eb --- /dev/null +++ b/deno.json @@ -0,0 +1,8 @@ +{ + "imports": { + "std/": "https://deno.land/std@0.207.0/" + }, + "tasks": { + "dev": "deno run --watch main.ts" + } +} diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..3a4d37d --- /dev/null +++ b/deno.lock @@ -0,0 +1,17 @@ +{ + "version": "3", + "redirects": { + "https://deno.land/x/expect/mod.ts": "https://deno.land/x/expect@v0.4.0/mod.ts" + }, + "remote": { + "https://deno.land/std@0.207.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c", + "https://deno.land/std@0.207.0/testing/bdd.ts": "3f446df5ef8e856a869e8eec54c8482590415741ff0b6358a00c43486cc15769", + "https://deno.land/std@0.97.0/fmt/colors.ts": "db22b314a2ae9430ae7460ce005e0a7130e23ae1c999157e3bb77cf55800f7e4", + "https://deno.land/std@0.97.0/testing/_diff.ts": "961eaf6d9f5b0a8556c9d835bbc6fa74f5addd7d3b02728ba7936ff93364f7a3", + "https://deno.land/std@0.97.0/testing/asserts.ts": "341292d12eebc44be4c3c2ca101ba8b6b5859cef2fa69d50c217f9d0bfbcfd1f", + "https://deno.land/x/expect@v0.4.0/expect.ts": "1d1856758a750f440d0b65d74f19e5d4829bb76d8e576d05546abd8e7b1dfb9e", + "https://deno.land/x/expect@v0.4.0/matchers.ts": "55acf74a3c4a308d079798930f05ab11da2080ec7acd53517193ca90d1296bf7", + "https://deno.land/x/expect@v0.4.0/mock.ts": "562d4b1d735d15b0b8e935f342679096b64fe452f86e96714fe8616c0c884914", + "https://deno.land/x/expect@v0.4.0/mod.ts": "0304d2430e1e96ba669a8495e24ba606dcc3d152e1f81aaa8da898cea24e36c2" + } +} diff --git a/examples/deno.ts b/examples/deno.ts index 992693d..b973f7e 100644 --- a/examples/deno.ts +++ b/examples/deno.ts @@ -1,5 +1,5 @@ // import { parse, mustEnd } from 'https://unpkg.com/yieldparser@0.4.0?module'; -import { parse, mustEnd } from '../src/index.ts'; +import { mustEnd, parse } from "../src/index.ts"; function* Digit() { const [digit]: [string] = yield /^\d+/; @@ -12,15 +12,15 @@ function* Digit() { function* IPAddress() { const first = yield Digit; - yield '.'; + yield "."; const second = yield Digit; - yield '.'; + yield "."; const third = yield Digit; - yield '.'; + yield "."; const fourth = yield Digit; yield mustEnd; return [first, second, third, fourth]; } -console.log(parse('1.2.3.4', IPAddress())); -console.log(parse('1.2.3.256', IPAddress())); +console.log(parse("1.2.3.4", IPAddress())); +console.log(parse("1.2.3.256", IPAddress())); diff --git a/jest.config.js b/jest.config.js index 0a6a68f..ea9e64c 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,6 +1,6 @@ module.exports = { - testEnvironment: 'node', + testEnvironment: "node", transform: { - '^.+\\.(t|j)sx?$': '@swc/jest', + "^.+\\.(t|j)sx?$": "@swc/jest", }, }; diff --git a/src/index.test.ts b/src/index.test.ts index 5c871a1..d383321 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,276 +1,279 @@ -import { parse, hasMore, mustEnd, has } from './index'; -import type { ParseGenerator } from './index'; - -describe('parse()', () => { - describe('failing', () => { - test('array of wrong substrings', () => { - expect(parse('abcdef', ['abc', 'wrong'])).toEqual({ - remaining: 'def', +import { describe, expect, it } from "./test-deps.ts"; +import { has, hasMore, mustEnd, parse } from "./index.ts"; +import type { ParsedType, ParseGenerator } from "./index.ts"; + +const test = Deno.test; + +describe("parse()", () => { + describe("failing", () => { + test("array of wrong substrings", () => { + expect(parse("abcdef", ["abc", "wrong"])).toEqual({ + remaining: "def", success: false, - failedOn: { iterationCount: 1, yielded: 'wrong' }, + failedOn: { iterationCount: 1, yielded: "wrong" }, }); }); - test('yielding string after start', () => { + test("yielding string after start", () => { expect( parse( - 'abc', + "abc", (function* () { - yield 'bc'; - })() - ) + yield "bc"; + })(), + ), ).toEqual({ success: false, - remaining: 'abc', - failedOn: { iterationCount: 0, yielded: 'bc' }, + remaining: "abc", + failedOn: { iterationCount: 0, yielded: "bc" }, }); }); - test('yielding wrong string', () => { + test("yielding wrong string", () => { expect( parse( - 'abcDEF', + "abcDEF", (function* () { - yield 'abc'; - yield 'def'; - })() - ) + yield "abc"; + yield "def"; + })(), + ), ).toEqual({ success: false, - remaining: 'DEF', - failedOn: { iterationCount: 1, yielded: 'def' }, + remaining: "DEF", + failedOn: { iterationCount: 1, yielded: "def" }, }); }); }); - describe('succeeding iterables', () => { - it('accepts substrings', () => { - expect(parse('abcdef', ['abc', 'def'])).toEqual({ - remaining: '', + describe("succeeding iterables", () => { + it("accepts substrings", () => { + expect(parse("abcdef", ["abc", "def"])).toEqual({ + remaining: "", success: true, }); }); - it('accepts array of substrings', () => { - expect(parse('abcdef', [['123', 'abc'], 'def'])).toEqual({ - remaining: '', + it("accepts array of substrings", () => { + expect(parse("abcdef", [["123", "abc"], "def"])).toEqual({ + remaining: "", success: true, }); }); - it('only replaces first match', () => { - expect(parse('abc123abc', ['abc', '123', 'abc'])).toEqual({ - remaining: '', + it("only replaces first match", () => { + expect(parse("abc123abc", ["abc", "123", "abc"])).toEqual({ + remaining: "", success: true, }); }); }); - describe('succeeding generator functions', () => { - it('accepts substrings', () => { + describe("succeeding generator functions", () => { + it("accepts substrings", () => { expect( parse( - 'abcdef', + "abcdef", (function* () { - yield 'abc'; - yield 'def'; - })() - ) + yield "abc"; + yield "def"; + })(), + ), ).toEqual({ - remaining: '', + remaining: "", success: true, }); }); - it('accepts empty string', () => { + it("accepts empty string", () => { expect( parse( - 'abcdef', + "abcdef", (function* () { - yield ''; - yield 'abc'; - yield ''; - yield 'def'; - yield ''; - })() - ) + yield ""; + yield "abc"; + yield ""; + yield "def"; + yield ""; + })(), + ), ).toEqual({ - remaining: '', + remaining: "", success: true, }); }); - it('accepts array of substrings', () => { + it("accepts array of substrings", () => { expect( parse( - 'abcdef', + "abcdef", (function* () { - const found: string = yield ['abc', '123']; - yield 'def'; + const found: string = yield ["abc", "123"]; + yield "def"; return { found }; - })() - ) + })(), + ), ).toEqual({ - remaining: '', + remaining: "", success: true, result: { - found: 'abc', + found: "abc", }, }); }); - it('accepts array of substrings', () => { + it("accepts array of substrings", () => { expect( parse( - 'abcdef', + "abcdef", (function* () { - const found: string = yield ['123', 'abc']; - yield 'def'; + const found: string = yield ["123", "abc"]; + yield "def"; return { found }; - })() - ) + })(), + ), ).toEqual({ - remaining: '', + remaining: "", success: true, result: { - found: 'abc', + found: "abc", }, }); }); - it('accepts Set of substrings', () => { + it("accepts Set of substrings", () => { expect( parse( - 'abcdef', + "abcdef", (function* () { - const found: string = yield new Set(['123', 'abc']); - yield 'def'; + const found: string = yield new Set(["123", "abc"]); + yield "def"; return { found }; - })() - ) + })(), + ), ).toEqual({ - remaining: '', + remaining: "", success: true, result: { - found: 'abc', + found: "abc", }, }); }); - it('accepts Set of substrings', () => { + it("accepts Set of substrings", () => { expect( parse( - 'abcdef', + "abcdef", (function* () { - const found: string = yield 'abc'; - yield 'def'; + const found: string = yield "abc"; + yield "def"; return { found }; - })() - ) + })(), + ), ).toEqual({ - remaining: '', + remaining: "", success: true, result: { - found: 'abc', + found: "abc", }, }); }); - it('accepts regex', () => { + it("accepts regex", () => { expect( parse( - 'abcdef', + "abcdef", (function* () { yield /^abc/; yield /^def$/; - })() - ) + })(), + ), ).toEqual({ - remaining: '', + remaining: "", success: true, }); }); - it('accepts newlines as string and regex', () => { + it("accepts newlines as string and regex", () => { expect( parse( - '\n\n', + "\n\n", (function* () { - yield '\n'; + yield "\n"; yield /^\n/; - })() - ) + })(), + ), ).toEqual({ - remaining: '', + remaining: "", success: true, }); }); - it('yields result from regex', () => { + it("yields result from regex", () => { expect( parse( - 'abcdef', + "abcdef", (function* () { const [found1]: [string] = yield /^abc/; const [found2]: [string] = yield /^def/; return { found1, found2 }; - })() - ) + })(), + ), ).toEqual({ - remaining: '', + remaining: "", success: true, result: { - found1: 'abc', - found2: 'def', + found1: "abc", + found2: "def", }, }); }); - it('accepts regex with capture groups', () => { + it("accepts regex with capture groups", () => { expect( parse( - 'abcdef', + "abcdef", (function* () { const [whole, first, second]: [ string, string, - string + string, ] = yield /^a(b)(c)/; const [found2]: [string] = yield /^def/; return { whole, first, second, found2 }; - })() - ) + })(), + ), ).toEqual({ - remaining: '', + remaining: "", success: true, result: { - whole: 'abc', - first: 'b', - second: 'c', - found2: 'def', + whole: "abc", + first: "b", + second: "c", + found2: "def", }, }); }); - it('accepts yield delegating to other generator function', () => { + it("accepts yield delegating to other generator function", () => { function* BCD() { - yield 'b'; - yield 'c'; - yield 'd'; + yield "b"; + yield "c"; + yield "d"; return { bcd: true }; } expect( parse( - 'abcdef', + "abcdef", (function* () { - yield 'a'; + yield "a"; const result = yield* BCD(); - yield 'ef'; + yield "ef"; return result; - })() - ) + })(), + ), ).toEqual({ - remaining: '', + remaining: "", success: true, result: { bcd: true, @@ -278,33 +281,33 @@ describe('parse()', () => { }); }); - it('accepts yielding array of other generator functions', () => { + it("accepts yielding array of other generator functions", () => { function* BCD() { - yield 'b'; - yield 'c'; - yield 'd'; + yield "b"; + yield "c"; + yield "d"; return { bcd: true }; } function* BAD() { - yield 'b'; - yield 'a'; - yield 'd'; + yield "b"; + yield "a"; + yield "d"; return { bad: true }; } expect( parse( - 'abcdef', + "abcdef", (function* () { - yield 'a'; + yield "a"; const result = yield [BAD, BCD]; - yield 'ef'; + yield "ef"; return result; - })() - ) + })(), + ), ).toEqual({ - remaining: '', + remaining: "", success: true, result: { bcd: true, @@ -313,7 +316,7 @@ describe('parse()', () => { }); }); - describe('IP Address', () => { + describe("IP Address", () => { function* Digit() { const [digit]: [string] = yield /^\d+/; const value = parseInt(digit, 10); @@ -324,55 +327,49 @@ describe('parse()', () => { } function* IPAddress() { - const first = yield Digit; - yield '.'; - const second = yield Digit; - yield '.'; - const third = yield Digit; - yield '.'; - const fourth = yield Digit; + const first: number = yield Digit; + yield "."; + const second: number = yield Digit; + yield "."; + const third: number = yield Digit; + yield "."; + const fourth: number = yield Digit; yield mustEnd; return [first, second, third, fourth]; } - it('accepts valid IP addresses', () => { - expect(parse('1.2.3.4', IPAddress())).toEqual({ + it("accepts valid IP addresses", () => { + expect(parse("1.2.3.4", IPAddress())).toEqual({ success: true, result: [1, 2, 3, 4], - remaining: '', + remaining: "", }); - expect(parse('255.255.255.255', IPAddress())).toEqual({ + expect(parse("255.255.255.255", IPAddress())).toEqual({ success: true, result: [255, 255, 255, 255], - remaining: '', + remaining: "", }); }); - it('rejects invalid IP addresses', () => { - expect(parse('1.2.3.256', IPAddress())).toEqual({ - success: false, - failedOn: expect.objectContaining({ - nested: [ - expect.objectContaining({ - yielded: new Error('Digit must be between 0 and 255, was 256'), - }), - ], - }), - remaining: '256', - }); + it("rejects invalid 1.2.3.256", () => { + const result = parse("1.2.3.256", IPAddress()); + expect(result.success).toBe(false); + expect(result.remaining).toBe("256"); + expect((result as any).failedOn.nested.yield).toEqual( + new Error("Digit must be between 0 and 255, was 256"), + ); + }); - expect(parse('1.2.3.4.5', IPAddress())).toEqual({ - success: false, - failedOn: expect.objectContaining({ - yielded: mustEnd, - }), - remaining: '.5', - }); + it("rejects invalid 1.2.3.4.5", () => { + const result = parse("1.2.3.4.5", IPAddress()); + expect(result.success).toBe(false); + expect(result.remaining).toBe(".5"); + expect((result as any).failedOn.nested.yield).toEqual(mustEnd); }); }); - describe('CSS', () => { + describe("CSS", () => { type Selector = string; interface Declaraction { property: string; @@ -396,27 +393,35 @@ describe('parse()', () => { } function* DeclarationParser() { - const name = yield PropertyParser; + const name: string = yield PropertyParser; yield whitespaceMay; - yield ':'; + yield ":"; yield whitespaceMay; - const rawValue = yield ValueParser; + const rawValue: string = yield ValueParser; yield whitespaceMay; - yield ';'; + yield ";"; return { name, rawValue }; } - function* RuleParser() { + function* RuleParser(): + | Generator> + | Generator<() => ParseGenerator, Rule, boolean> + | Generator< + () => typeof DeclarationParser, + Rule, + ParsedType + > + | Generator { const declarations: Array = []; const [selector]: [string] = yield /^(:root|[*]|[a-z][\w]*)/; yield whitespaceMay; - yield '{'; + yield "{"; yield whitespaceMay; - while ((yield has('}')) === false) { + while ((yield has("}")) === false) { yield whitespaceMay; - declarations.push(yield DeclarationParser); + declarations.push((yield DeclarationParser) as unknown as Declaraction); yield whitespaceMay; } @@ -450,47 +455,47 @@ describe('parse()', () => { } `; - it('parses', () => { + it("parses", () => { expect(parse(code, RulesParser())).toEqual({ success: true, result: [ { - selectors: [':root'], + selectors: [":root"], declarations: [ { - name: '--first-var', - rawValue: '42rem', + name: "--first-var", + rawValue: "42rem", }, { - name: '--second-var', - rawValue: '15%', + name: "--second-var", + rawValue: "15%", }, ], }, { - selectors: ['*'], + selectors: ["*"], declarations: [ { - name: 'font', - rawValue: 'inherit', + name: "font", + rawValue: "inherit", }, { - name: 'box-sizing', - rawValue: 'border-box', + name: "box-sizing", + rawValue: "border-box", }, ], }, { - selectors: ['h1'], + selectors: ["h1"], declarations: [ { - name: 'margin-bottom', - rawValue: '1em', + name: "margin-bottom", + rawValue: "1em", }, ], }, ], - remaining: '', + remaining: "", }); }); }); diff --git a/src/index.ts b/src/index.ts index 08e35d7..ab659f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,19 @@ +export type ParsedType = A extends { Parser: () => Generator } + ? ParsedTypeForClass + : A extends (...args: unknown[]) => unknown ? ParsedTypeForFunction + : never; +type ParsedTypeForFunction unknown> = + ReturnType extends Generator ? Y : never; +type ParsedTypeForClass Generator }> = ReturnType< + C["Parser"] +> extends Generator ? Y + : never; + export type ParseItem = | string | RegExp | Iterable - | (() => Generator); + | (() => Generator); export type ParseYieldable = ParseItem; export interface ParseError { @@ -13,28 +24,29 @@ export interface ParseError { export type ParseResult = | { - success: false; - remaining: string; - failedOn: ParseError; - } + success: false; + remaining: string; + failedOn: ParseError; + } | { - success: true; - remaining: string; - result: Result; - }; + success: true; + remaining: string; + result: Result; + }; export type ParseYieldedValue = Input extends RegExp ? RegExpMatchArray : string; export type ParseGenerator = - | Generator, Result, string | RegExpMatchArray> + | Generator, Result, string | RegExpMatchArray> + | Generator, Result, unknown> | Generator | Iterable; export function parse( input: string, - iterable: ParseGenerator + iterable: ParseGenerator, ): ParseResult { let lastResult: ParseYieldedValue | undefined; @@ -67,16 +79,16 @@ export function parse( const yielded = next.value as ParseItem; const choices = - typeof yielded !== 'string' && (yielded as any)[Symbol.iterator] + typeof yielded !== "string" && (yielded as any)[Symbol.iterator] ? (yielded as Iterable) : [yielded]; for (const choice of choices) { - if (typeof choice === 'string') { + if (typeof choice === "string") { let found = false; const newInput = input.replace(choice, (_1, offset: number) => { found = offset === 0; - return ''; + return ""; }); if (found) { input = newInput; @@ -84,7 +96,7 @@ export function parse( continue main; } } else if (choice instanceof RegExp) { - if (['^', '$'].includes(choice.source[0]) === false) { + if (["^", "$"].includes(choice.source[0]) === false) { throw new Error(`Regex must be from start: ${choice}`); } const match = input.match(choice); @@ -146,18 +158,22 @@ export function* hasMore() { export function has(prefix: ParseYieldable): () => ParseGenerator { return function* () { - return (yield [prefix, '']) !== ''; + return (yield [prefix, ""]) !== ""; }; } -export function optional(...potentials: Array): () => ParseGenerator { +export function optional( + ...potentials: Array +): () => ParseGenerator { return function* () { - const result = yield [...potentials, '']; - return result === '' ? undefined : result; + const result = yield [...potentials, ""]; + return result === "" ? undefined : result; }; } -export function lookAhead(regex: RegExp) { +export function lookAhead( + regex: RegExp, +): () => Generator { const lookAheadRegex = new RegExp(`^(?=${regex.source})`); return function* () { return yield lookAheadRegex; @@ -170,18 +186,18 @@ export function invert( needle: {}, iterable: ParseGenerator, ): string | null { - const result = invertInner(needle, iterable); - if (result !== null && result.type === 'done') { - return result.components.join(''); + const result = invertInner(needle, iterable); + if (result !== null && result.type === "done") { + return result.components.join(""); } return null; } function invertInner( - needle: {}, + needle: Record, iterable: ParseGenerator, -): { type: 'done' | 'prefix'; components: ReadonlyArray } | null { +): { type: "done" | "prefix"; components: ReadonlyArray } | null { let reply: unknown | undefined; const expectedKeys = Object.keys(needle); @@ -201,7 +217,7 @@ function invertInner( const result = next.value; if (result == null) { - return { type: 'prefix', components: Object.freeze(components) }; + return { type: "prefix", components: Object.freeze(components) }; } const resultKeys = new Set(Object.keys(result)); @@ -212,10 +228,12 @@ function invertInner( return false; } - if (typeof result[key] === 'symbol') { + if (typeof result[key] === "symbol") { const entry = regexpMap.get(result[key]); if (entry !== undefined) { - if (entry.regexp.test(needle[key])) { + if ( + entry.regexp.test(needle[key]) + ) { components[entry.index] = needle[key]; return true; } @@ -225,7 +243,7 @@ function invertInner( return result[key] === needle[key]; }) ) { - return { type: 'done', components: Object.freeze(components) }; + return { type: "done", components: Object.freeze(components) }; } else { return null; } @@ -246,7 +264,7 @@ function invertInner( break; // Assume first string is the canonical version. } else if (choice instanceof RegExp) { const index = components.length; - components.push(''); // This will be replaced later using the index. + components.push(""); // This will be replaced later using the index. // components.push('???'); // This will be replaced later using the index. const s = Symbol(); regexpMap.set(s, { regexp: choice, index }); @@ -254,8 +272,11 @@ function invertInner( } else if (choice instanceof Function) { const result = invertInner(needle, choice()); if (result != null) { - if (result.type === 'done') { - return { type: 'done', components: Object.freeze(components.concat(result.components)) }; + if (result.type === "done") { + return { + type: "done", + components: Object.freeze(components.concat(result.components)), + }; } else { components.push(...result.components); } @@ -264,3 +285,40 @@ function invertInner( } } } + + +// type CustomFunc = (p: Parser) => T; + +// interface MatcherFunc { +// (s: string): string; +// (r: RegExp): [string]; +// (c: CustomFunc): T; +// } + +// type Parser = MatcherFunc & { +// peek: MatcherFunc; +// error(description: string): void; +// }; + +// function Digit(this: Parser): number { +// const [digits] = this(/^\d+$/); +// const value = parseInt(digits, 10); + +// if (value < 0 || value > 255) { +// this.error(`value must be between 0 and 255, was ${value}`); +// } + +// return value; +// } + +// function IPAddress(this: Parser): [number, number, number, number] { +// const first = this(Digit); +// this("."); +// const second = this(Digit); +// this("."); +// const third = this(Digit); +// this("."); +// const fourth = this(Digit); + +// return [first, second, third, fourth]; +// } diff --git a/src/math.test.ts b/src/math.test.ts index fcb626e..a8109bc 100644 --- a/src/math.test.ts +++ b/src/math.test.ts @@ -1,30 +1,31 @@ -import { parse, hasMore, has, ParseGenerator } from './index'; +import { afterEach, beforeEach, describe, expect, it } from "./test-deps.ts"; +import { has, hasMore, parse, ParseGenerator } from "./index.ts"; -describe('math parser', () => { +describe("math parser", () => { const whitespaceMay = /^\s*/; function* ParseInt() { - const isNegative: boolean = yield has('-'); + const isNegative: boolean = yield has("-"); const [stringValue]: [string] = yield /^\d+/; return parseInt(stringValue, 10) * (isNegative ? -1 : 1); } - type Operator = '+' | '-' | '*' | '/'; + type Operator = "+" | "-" | "*" | "/"; function* ParseOperator() { - const operator: Operator = yield ['+', '-', '*', '/']; + const operator: Operator = yield ["+", "-", "*", "/"]; return operator; } function applyOperator(a: number, b: number, operator: Operator): number { switch (operator) { - case '+': + case "+": return a + b; - case '-': + case "-": return a - b; - case '*': + case "*": return a * b; - case '/': + case "/": return a / b; } } @@ -45,28 +46,30 @@ describe('math parser', () => { return current; } - test.each([ - ['1 + 1', 2], - ['1 + 2', 3], - ['2 + 2', 4], - ['21 + 19', 40], - ['21 + -19', 2], - ['-21 + 19', -2], - ['-21 + -19', -40], - ['0 - 10', -10], - ['21 - 19', 2], - ['-21 - 19', -40], - ['1 * 1', 1], - ['2 * 2', 4], - ['12 * 12', 144], - ['1 / 2', 0.5], - ['10 / 2', 5], - ['10 / 20', 0.5], - ])('%o', (input: string, output: number) => { - expect(parse(input, MathExpression())).toEqual({ - success: true, - result: output, - remaining: '', + Deno.test("many", () => { + ([ + ["1 + 1", 2], + ["1 + 2", 3], + ["2 + 2", 4], + ["21 + 19", 40], + ["21 + -19", 2], + ["-21 + 19", -2], + ["-21 + -19", -40], + ["0 - 10", -10], + ["21 - 19", 2], + ["-21 - 19", -40], + ["1 * 1", 1], + ["2 * 2", 4], + ["12 * 12", 144], + ["1 / 2", 0.5], + ["10 / 2", 5], + ["10 / 20", 0.5], + ] as const).forEach(([input, output]) => { + expect(parse(input, MathExpression())).toEqual({ + success: true, + result: output, + remaining: "", + }); }); }); }); diff --git a/src/media-query.test.ts b/src/media-query.test.ts index 35953eb..2cc06bc 100644 --- a/src/media-query.test.ts +++ b/src/media-query.test.ts @@ -1,34 +1,34 @@ // https://www.w3.org/TR/mediaqueries-5/ +import { afterEach, beforeEach, describe, expect, it } from './test-deps.ts'; import { - has, - hasMore, mustEnd, optional, parse, + ParsedType, ParseGenerator, ParseResult, ParseYieldable, -} from './index'; +} from './index.ts'; const optionalWhitespace = /^\s*/; const requiredWhitespace = /^\s+/; -type ParsedType = A extends { Parser: () => Generator } - ? ParsedTypeForClass - : A extends (...args: unknown[]) => unknown - ? ParsedTypeForFunction - : never; -type ParsedTypeForFunction unknown> = - ReturnType extends Generator ? Y : never; -type ParsedTypeForClass Generator }> = ReturnType< - C['Parser'] -> extends Generator - ? Y - : never; +export function has(prefix: string | RegExp): () => ParserGenerator { + return function* (): ParserGenerator { + const [match] = yield [prefix, '']; + return match !== ''; + }; +} -function* ParseInt() { - const isNegative: boolean = yield has('-'); - const [stringValue]: [string] = yield /^\d+/; +export function* hasMore(): ParserGenerator { + const { index }: { index: number } = yield /$/; + return index > 0; + // return !(yield isEnd); +} + +function* ParseInt(): ParserGenerator { + const isNegative = Boolean(yield has('-')); + const [stringValue] = yield /^\d+/; return parseInt(stringValue, 10) * (isNegative ? -1 : 1); } @@ -50,11 +50,11 @@ class ParsedMediaType { return this.mediaType === context.mediaType; } - static *Parser() { + static *Parser(): ParserGenerator { yield optionalWhitespace; - yield optional(() => ['only', requiredWhitespace]); - const mediaType: ParsedMediaType['mediaType'] = yield ['screen', 'print']; - return new ParsedMediaType(mediaType); + yield /^only\s+/; + const [mediaType] = yield ['screen', 'print']; + return new ParsedMediaType(mediaType as 'screen' | 'print'); } } @@ -66,15 +66,12 @@ class ParsedNotMediaType { return this.mediaType !== context.mediaType; } - static *Parser() { + static *Parser(): ParserGenerator { yield optionalWhitespace; yield 'not'; yield requiredWhitespace; - const mediaType: ParsedNotMediaType['mediaType'] = yield [ - 'screen', - 'print', - ]; - return new ParsedNotMediaType(mediaType); + const [mediaType] = yield ['screen', 'print']; + return new ParsedNotMediaType(mediaType as ParsedNotMediaType['mediaType']); } } @@ -101,17 +98,17 @@ class ParsedMinWidth { return this.valueInPx(context) <= context.viewportWidth; } - static *Parser() { + static *Parser(): ParserGenerator { yield optionalWhitespace; yield '('; yield optionalWhitespace; yield 'min-width:'; yield optionalWhitespace; - const value: number = yield ParseInt; - const unit = yield ['px', 'em', 'rem']; + const { value } = yield ParseInt; + const [unit] = yield ['px', 'em', 'rem']; yield optionalWhitespace; yield ')'; - return new ParsedMinWidth(value, unit); + return new ParsedMinWidth(value.valueOf(), unit as 'px' | 'em' | 'rem'); } } @@ -129,19 +126,16 @@ class ParsedOrientation { return this.orientation === calculated; } - static *Parser() { + static *Parser(): ParserGenerator { yield optionalWhitespace; yield '('; yield optionalWhitespace; yield 'orientation:'; yield optionalWhitespace; - const orientation: 'portrait' | 'landscape' = yield [ - 'portrait', - 'landscape', - ]; + const [orientation] = yield ['portrait', 'landscape']; yield optionalWhitespace; yield ')'; - return new ParsedOrientation(orientation); + return new ParsedOrientation(orientation as 'portrait' | 'landscape'); } } @@ -164,7 +158,7 @@ const PointerAccuracy = Object.freeze({ } }, }); -type PointerLevels = typeof PointerAccuracy['none' | 'coarse' | 'fine']; +type PointerLevels = (typeof PointerAccuracy)['none' | 'coarse' | 'fine']; class ParsedPointer { constructor( public readonly accuracy: 'none' | 'coarse' | 'fine', @@ -200,17 +194,20 @@ class ParsedPointer { return deviceLevel >= minLevel; } - static *Parser() { + static *Parser(): ParserGenerator { yield optionalWhitespace; yield '('; yield optionalWhitespace; - const any: boolean = yield has('any-'); + const any = Boolean(yield has('any-')); yield 'pointer:'; yield optionalWhitespace; - const hover: 'none' | 'coarse' | 'fine' = yield ['none', 'coarse', 'fine']; + const [hover] = yield ['none', 'coarse', 'fine']; yield optionalWhitespace; yield ')'; - return new ParsedPointer(hover, any ? 'any' : undefined); + return new ParsedPointer( + hover as 'none' | 'coarse' | 'fine', + any ? 'any' : undefined + ); } } @@ -254,17 +251,17 @@ class ParsedHover { } } - static *Parser() { + static *Parser(): ParserGenerator { yield optionalWhitespace; yield '('; yield optionalWhitespace; - const any: boolean = yield has('any-'); + const any = Boolean(yield has('any-')); yield 'hover:'; yield optionalWhitespace; - const hover: 'none' | 'hover' = yield ['none', 'hover']; + const [hover] = yield ['none', 'hover']; yield optionalWhitespace; yield ')'; - return new ParsedHover(hover, any ? 'any' : undefined); + return new ParsedHover(hover as 'none' | 'hover', any ? 'any' : undefined); } } @@ -276,7 +273,12 @@ const parsedMediaFeature = [ ParsedPointer.Parser, ]; const parsedMediaInParens = [...parsedMediaFeature]; -type ParsedMediaFeature = ParsedType; +// type ParsedMediaFeature = ParsedType<(typeof parsedMediaFeature)[-1]>; +type ParsedMediaFeature = + | ParsedMinWidth + | ParsedOrientation + | ParsedHover + | ParsedPointer; type ParsedMediaInParens = ParsedMediaFeature; class ParsedMediaCondition { @@ -312,6 +314,127 @@ class ParsedMediaCondition { } } +type GetYield = T extends { + next(...args: [unknown]): IteratorResult; +} + ? A + : never; + +const internal = Symbol('internal'); +class YieldedValue { + constructor(stringValue: string) { + this[internal] = stringValue; + } + + get value(): T { + return this[internal]; + } + + get index(): number { + return 0; + } + + *[Symbol.iterator](): IterableIterator { + const a: Array = this[internal]; + yield* a; + } +} + +type PrimitiveYield = + | S + | RegExp + // | (() => Omit, "next" | "return" | "throw">) + // | (() => Omit, "next" | "return" | "throw">) + | (() => { + [Symbol.iterator](): { + next: { + (result: unknown): IteratorResult; + // (result: unknown): IteratorResult + }; + }; + }) + | Array>; + +type Next = { + // next: { + // (s: string): IteratorResult; + // (matches: [string]): IteratorResult; + // }; + next: { + // (result: YieldedValue): IteratorResult< + // PrimitiveYield | (() => Generator), + // Result + // >; + (result: YieldedValue): IteratorResult< + typeof result extends YieldedValue + ? Z extends string + ? PrimitiveYield + : PrimitiveYield + : PrimitiveYield, + Result + >; + // (result: YieldedValue): IteratorResult, Result>; + // (result: A): A extends string + // ? IteratorResult + // : A extends Iterable + // ? IteratorResult + // : never; + }; +}; +// | { +// next( +// ...args: [boolean] +// ): IteratorResult<() => Generator, Result>; +// } +// | { +// next(...args: [T]): IteratorResult<() => Generator, Result>; +// } +// & { +// next(s: string): IteratorResult; +// } +// & { +// next(matches: [string]): IteratorResult; +// } +// & { +// next(): IteratorResult; +// }; +// type Next = GetYield extends RegExp ? { +// next( +// ...args: [[string] & ReadonlyArray] +// ): IteratorResult; +// } : GetYield extends string ? { +// next( +// ...args: [string] +// ): IteratorResult; +// } : never; + +// type Next = { +// next( +// ...args: [[string] & ReadonlyArray] +// ): IteratorResult; +// next( +// ...args: [string] +// ): IteratorResult; +// }; +// type Next = T extends RegExp ? { +// next( +// ...args: [[string] & ReadonlyArray] +// ): IteratorResult; +// } +// : T extends string ? { +// next( +// ...args: [string] +// ): IteratorResult; +// } +// : never; + +type ParserGenerator< + Result, + NextValue extends object | number | boolean = never +> = { + [Symbol.iterator](): Next; +}; + class ParsedMediaAnds { constructor(public readonly list: ReadonlyArray) {} @@ -319,14 +442,15 @@ class ParsedMediaAnds { return this.list.every((m) => m.matches(context)); } - static *Parser() { + static *Parser(): ParserGenerator { const list: Array = []; do { + const [a, c] = yield requiredWhitespace; + const [b] = yield 'and'; yield requiredWhitespace; - yield 'and'; - yield requiredWhitespace; - list.push(yield parsedMediaInParens); + const { value: item } = yield parsedMediaInParens; + list.push(item); } while (yield hasMore); return new ParsedMediaAnds(list); @@ -340,14 +464,14 @@ class ParsedMediaOrs { return this.list.some((m) => m.matches(context)); } - static *Parser() { + static *Parser(): ParserGenerator { const list: Array = []; do { yield requiredWhitespace; yield 'or'; yield requiredWhitespace; - list.push(yield parsedMediaInParens); + list.push((yield parsedMediaInParens).value); } while (yield hasMore); return new ParsedMediaOrs(list); @@ -367,22 +491,46 @@ class ParsedMediaTypeThenConditionWithoutOr { ); } - static *Parser() { - const mediaType: ParsedMediaType | ParsedNotMediaType = yield [ + static *ParserA(): ParserGenerator< + | ParsedMediaType + | ParsedNotMediaType + | ParsedMediaTypeThenConditionWithoutOr, + ParsedMediaType | ParsedNotMediaType + > { + const mediaType = yield [ParsedMediaType.Parser, ParsedNotMediaType.Parser]; + + const list: Array = []; + + if (list.length === 0) { + return mediaType.value; + } else { + return new ParsedMediaTypeThenConditionWithoutOr(mediaType.value, list); + } + } + + static *Parser(): ParserGenerator< + | ParsedMediaType + | ParsedNotMediaType + | ParsedMediaTypeThenConditionWithoutOr, + ParsedMediaType | ParsedNotMediaType | ParsedMediaInParens + > { + const mediaType = (yield [ ParsedMediaType.Parser, ParsedNotMediaType.Parser, - ]; + ]) as YieldedValue; const list: Array = []; while (yield has(/^\s+and\s/)) { - list.push(yield parsedMediaInParens); + list.push( + ((yield parsedMediaInParens) as YieldedValue).value + ); } if (list.length === 0) { - return mediaType; + return mediaType.value; } else { - return new ParsedMediaTypeThenConditionWithoutOr(mediaType, list); + return new ParsedMediaTypeThenConditionWithoutOr(mediaType.value, list); } } } @@ -406,7 +554,7 @@ class ParsedMediaQuery { } function matchMedia(context: MatchMediaContext, mediaQuery: string) { - const parsed: ParseResult = parse( + const parsed: ParseResult = parse( mediaQuery, ParsedMediaQuery.Parser() as any ); @@ -427,7 +575,7 @@ function matchMedia(context: MatchMediaContext, mediaQuery: string) { }; } -test('screen', () => { +it('can parse "screen"', () => { const result = parse('screen', ParsedMediaQuery.Parser() as any); expect(result).toEqual({ success: true, @@ -436,7 +584,7 @@ test('screen', () => { }); }); -test('(min-width: 480px)', () => { +it('can parse (min-width: 480px)', () => { const result = parse('(min-width: 480px)', ParsedMediaQuery.Parser() as any); expect(result).toEqual({ success: true, @@ -445,7 +593,7 @@ test('(min-width: 480px)', () => { }); }); -test('(orientation: landscape)', () => { +it('can parse (orientation: landscape)', () => { const result = parse( '(orientation: landscape)', ParsedMediaQuery.Parser() as any @@ -457,7 +605,7 @@ test('(orientation: landscape)', () => { }); }); -test('screen and (min-width: 480px)', () => { +it('can parse "screen and (min-width: 480px)"', () => { const result = parse( 'screen and (min-width: 480px)', ParsedMediaQuery.Parser() as any @@ -472,7 +620,7 @@ test('screen and (min-width: 480px)', () => { }); }); -test('matchMedia()', () => { +it('can run matchMedia()', () => { const defaultRootFontSizePx = 16; const viewport = (width: number, height: number, zoom: number = 1) => ({ @@ -632,14 +780,16 @@ test('matchMedia()', () => { matchMedia(screenSized(100, 100, 'touchscreen'), '(pointer: fine)').matches ).toBe(false); expect( - matchMedia(screenSized(100, 100, 'touchscreen'), '(any-pointer: none)').matches + matchMedia(screenSized(100, 100, 'touchscreen'), '(any-pointer: none)') + .matches ).toBe(false); expect( matchMedia(screenSized(100, 100, 'touchscreen'), '(any-pointer: coarse)') .matches ).toBe(true); expect( - matchMedia(screenSized(100, 100, 'touchscreen'), '(any-pointer: fine)').matches + matchMedia(screenSized(100, 100, 'touchscreen'), '(any-pointer: fine)') + .matches ).toBe(false); expect( @@ -663,14 +813,22 @@ test('matchMedia()', () => { ).matches ).toBe(true); expect( - matchMedia(screenSized(100, 100, 'touchscreen', 'mouse'), '(any-pointer: none)').matches + matchMedia( + screenSized(100, 100, 'touchscreen', 'mouse'), + '(any-pointer: none)' + ).matches ).toBe(false); expect( - matchMedia(screenSized(100, 100, 'touchscreen', 'mouse'), '(any-pointer: coarse)') - .matches + matchMedia( + screenSized(100, 100, 'touchscreen', 'mouse'), + '(any-pointer: coarse)' + ).matches ).toBe(true); expect( - matchMedia(screenSized(100, 100, 'touchscreen', 'mouse'), '(any-pointer: fine)').matches + matchMedia( + screenSized(100, 100, 'touchscreen', 'mouse'), + '(any-pointer: fine)' + ).matches ).toBe(true); expect( diff --git a/src/modules.test.ts b/src/modules.test.ts index 3a2a6bf..87dd57d 100644 --- a/src/modules.test.ts +++ b/src/modules.test.ts @@ -1,6 +1,7 @@ -import { parse, hasMore, has, optional } from './index'; +import { describe, expect, it } from "./test-deps.ts"; +import { has, hasMore, optional, parse } from "./index.ts"; -describe('ES modules', () => { +describe("ES modules", () => { const code = `import first from 'first-module'; import second from 'second-module'; @@ -23,7 +24,7 @@ function* oneTwoThree() { function closure() { function inner() {} - + return inner; } @@ -41,25 +42,26 @@ export function double() { const semicolonOptional = /^;*/; // See: https://stackoverflow.com/questions/2008279/validate-a-javascript-function-name const identifierRegex = /^[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*/; - const stringRegex = /^('(?([^']|\\['\\bfnrt\/])*)'|"(?([^"]|\\['\\bfnrt\/])*)")/; + const stringRegex = + /^('(?([^']|\\['\\bfnrt\/])*)'|"(?([^"]|\\['\\bfnrt\/])*)")/; function* Identifier() { const [name]: [string] = yield identifierRegex; - return { type: 'identifier', name }; + return { type: "identifier", name }; } function* StringLiteral() { const { groups, }: { - groups: Record<'contentsSingle' | 'contentsDouble', string>; + groups: Record<"contentsSingle" | "contentsDouble", string>; } = yield stringRegex; - return groups.contentsSingle || groups.contentsDouble || ''; + return groups.contentsSingle || groups.contentsDouble || ""; } function* NumberLiteral() { const [stringValue]: [ - string + string, ] = yield /^(([\d]+[.][\d]*)|([\d]*[.][\d]+)|([\d]+))/; return parseFloat(stringValue); } @@ -69,10 +71,10 @@ export function double() { } function* SymbolDeclaration() { - yield 'Symbol('; + yield "Symbol("; const name = yield StringLiteral; - yield ')'; - return { type: 'symbol', name }; + yield ")"; + return { type: "symbol", name }; } function* Expression() { @@ -80,47 +82,47 @@ export function double() { } function* ConstStatement() { - yield 'const'; + yield "const"; yield whitespaceMust; const { name }: { name: string } = yield Identifier; yield whitespaceMay; - yield '='; + yield "="; yield whitespaceMay; const value = yield Expression; yield semicolonOptional; - return { type: 'const', name, value }; + return { type: "const", name, value }; } function* ReturnStatement() { - yield 'return'; + yield "return"; yield whitespaceMust; const value = yield Expression; yield semicolonOptional; - return { type: 'return', value }; + return { type: "return", value }; } function* YieldStatement() { - yield 'yield'; + yield "yield"; yield whitespaceMust; const value = yield Expression; yield semicolonOptional; - return { type: 'yield', value }; + return { type: "yield", value }; } function* FunctionParser() { - yield 'function'; + yield "function"; yield whitespaceMay; - const isGenerator: boolean = yield has('*'); + const isGenerator: boolean = yield has("*"); yield whitespaceMay; const { name }: { name: string } = yield Identifier; yield whitespaceMay; - yield '('; - yield ')'; + yield "("; + yield ")"; yield whitespaceMay; - yield '{'; + yield "{"; yield whitespaceMay; let statements: Array = []; - while ((yield has('}')) === false) { + while ((yield has("}")) === false) { yield whitespaceMay; const statement = yield [ ConstStatement, @@ -132,30 +134,30 @@ export function double() { yield whitespaceMay; } // yield '}'; - return { type: 'function', name, isGenerator, statements }; + return { type: "function", name, isGenerator, statements }; } function* ImportStatement() { - yield 'import'; + yield "import"; yield whitespaceMust; const { name: defaultBinding }: { name: string } = yield Identifier; yield whitespaceMust; - yield 'from'; + yield "from"; yield whitespaceMay; const moduleSpecifier = yield StringLiteral; yield semicolonOptional; return { - type: 'import', + type: "import", defaultBinding, moduleSpecifier, }; } function* ExportStatement() { - yield 'export'; + yield "export"; yield whitespaceMust; const exported = yield [ConstStatement, FunctionParser]; - return { type: 'export', exported }; + return { type: "export", exported }; } // function* ExportNamed() { @@ -168,128 +170,133 @@ export function double() { while (yield hasMore) { yield /^[\s;]*/; lines.push( - yield [ConstStatement, ImportStatement, ExportStatement, FunctionParser] + yield [ + ConstStatement, + ImportStatement, + ExportStatement, + FunctionParser, + ], ); yield /^[\s;]*/; } return lines; } - it('accepts empty string', () => { - expect(parse('', ESModuleParser())).toEqual({ - remaining: '', + it("accepts empty string", () => { + expect(parse("", ESModuleParser())).toEqual({ + remaining: "", success: true, result: [], }); }); - describe('valid ES module', () => { + describe("valid ES module", () => { const expected = { - remaining: '', + remaining: "", success: true, result: [ { - type: 'import', - defaultBinding: 'first', - moduleSpecifier: 'first-module', + type: "import", + defaultBinding: "first", + moduleSpecifier: "first-module", }, { - type: 'import', - defaultBinding: 'second', - moduleSpecifier: 'second-module', + type: "import", + defaultBinding: "second", + moduleSpecifier: "second-module", }, { - type: 'const', - name: 'a', - value: 'hello!', + type: "const", + name: "a", + value: "hello!", }, { - type: 'const', - name: 'pi', + type: "const", + name: "pi", value: 3.14159, }, { - type: 'const', - name: 'symbolA', + type: "const", + name: "symbolA", value: { - type: 'symbol', - name: 'a' - } + type: "symbol", + name: "a", + }, }, { - type: 'function', - name: 'noop', + type: "function", + name: "noop", isGenerator: false, statements: [], }, { - type: 'function', - name: 'whoami', + type: "function", + name: "whoami", isGenerator: false, statements: [ { - type: 'return', - value: 'admin', + type: "return", + value: "admin", }, ], }, { - type: 'function', - name: 'oneTwoThree', + type: "function", + name: "oneTwoThree", isGenerator: true, statements: [ { - type: 'yield', + type: "yield", value: 1, }, { - type: 'yield', - value: 'some string', + type: "yield", + value: "some string", }, { - type: 'yield', + type: "yield", value: 3, }, ], }, { - type: 'function', - name: 'closure', + type: "function", + name: "closure", isGenerator: false, statements: [ { - type: 'function', - name: 'inner', + type: "function", + name: "inner", isGenerator: false, statements: [], }, { - type: 'return', + type: "return", value: { - type: 'identifier', - name: 'inner', + type: "identifier", + name: "inner", }, }, ], }, { - type: 'export', + type: "export", exported: { - type: 'const', - name: 'b', - value: 'some exported', + type: "const", + name: "b", + value: "some exported", }, }, { - type: 'export', + type: "export", exported: { - type: 'function', - name: 'double', + type: "function", + name: "double", isGenerator: false, statements: [ { - type: 'return', - value: 'double', + type: "return", + value: "double", }, ], }, @@ -297,17 +304,17 @@ export function double() { ], }; - it('can parse an ES module', () => { + it("can parse an ES module", () => { expect(parse(code, ESModuleParser())).toEqual(expected); }); - it('can parse with leading and trailing whitespace', () => { - expect(parse('\n \n ' + code + ' \n \n', ESModuleParser())).toEqual( - expected + it("can parse with leading and trailing whitespace", () => { + expect(parse("\n \n " + code + " \n \n", ESModuleParser())).toEqual( + expected, ); }); - describe('exports', () => { + describe("exports", () => { function* exports() { const result = parse(code, ESModuleParser()); if (!result.success) { @@ -315,23 +322,23 @@ export function double() { } for (const item of result.result as any[]) { - if (item.type === 'export') { + if (item.type === "export") { yield item.exported; } } } - it('exports b', () => { + it("exports b", () => { expect(Array.from(exports())).toEqual([ - { name: 'b', type: 'const', value: 'some exported' }, + { name: "b", type: "const", value: "some exported" }, { - type: 'function', - name: 'double', + type: "function", + name: "double", isGenerator: false, statements: [ { - type: 'return', - value: 'double', + type: "return", + value: "double", }, ], }, @@ -339,7 +346,7 @@ export function double() { }); }); - describe('lookup', () => { + describe("lookup", () => { function lookup(identifier: string) { const result = parse(code, ESModuleParser()); if (!result.success) { @@ -347,15 +354,15 @@ export function double() { } for (const item of result.result as any[]) { - if (item.type === 'const') { + if (item.type === "const") { if (item.name === identifier) { return item; } - } else if (item.type === 'function') { + } else if (item.type === "function") { if (item.name === identifier) { return item; } - } else if (item.type === 'export') { + } else if (item.type === "export") { if (item.exported.name === identifier) { return item.exported; } @@ -363,45 +370,45 @@ export function double() { } } - it('can lookup const', () => { - expect(lookup('a')).toEqual({ - type: 'const', - name: 'a', - value: 'hello!', + it("can lookup const", () => { + expect(lookup("a")).toEqual({ + type: "const", + name: "a", + value: "hello!", }); }); - it('can lookup function', () => { - expect(lookup('whoami')).toEqual({ - type: 'function', - name: 'whoami', + it("can lookup function", () => { + expect(lookup("whoami")).toEqual({ + type: "function", + name: "whoami", isGenerator: false, statements: [ { - type: 'return', - value: 'admin', + type: "return", + value: "admin", }, ], }); }); - it('can lookup exported const', () => { - expect(lookup('b')).toEqual({ - name: 'b', - type: 'const', - value: 'some exported', + it("can lookup exported const", () => { + expect(lookup("b")).toEqual({ + name: "b", + type: "const", + value: "some exported", }); }); - it('can lookup exported function', () => { - expect(lookup('double')).toEqual({ - type: 'function', - name: 'double', + it("can lookup exported function", () => { + expect(lookup("double")).toEqual({ + type: "function", + name: "double", isGenerator: false, statements: [ { - type: 'return', - value: 'double', + type: "return", + value: "double", }, ], }); diff --git a/src/natural-dates.test.ts b/src/natural-dates.test.ts index 4da3e82..ed01230 100644 --- a/src/natural-dates.test.ts +++ b/src/natural-dates.test.ts @@ -1,147 +1,249 @@ -import { has, optional, parse, ParseGenerator, ParseResult, ParseYieldable } from './index'; +import { describe, expect } from "./test-deps.ts"; +import { + has, + optional, + parse, + ParseGenerator, + ParseResult, + ParseYieldable, +} from "./index.ts"; -describe('natural date parser', () => { +describe("natural date parser", () => { const whitespaceOptional = /^\s*/; function* ParseInt() { const [stringValue]: [string] = yield /^\d+/; return parseInt(stringValue, 10); } - - const weekdayChoices = Object.freeze(['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] as const); + + const weekdayChoices = Object.freeze( + [ + "monday", + "tuesday", + "wednesday", + "thursday", + "friday", + "saturday", + "sunday", + ] as const, + ); type Weekday = (typeof weekdayChoices)[0 | 1 | 2 | 3 | 4 | 5 | 6]; - + function* WeekdayParser() { let repeats: boolean = yield has(/^every\b/); yield optional(/^next\b/); - + yield whitespaceOptional; - + const weekday: Weekday = yield weekdayChoices; repeats = repeats || (yield has(/^[s]\b/)); - + return { weekday, repeats }; } - + function* AnotherWeekdayParser() { yield whitespaceOptional; - yield optional('and', 'or'); + yield optional("and", "or"); yield whitespaceOptional; return yield WeekdayParser; } - + function* WeekdaysParser() { let repeats = false; - + const weekdays = new Set(); - - let result: { weekday: Weekday, repeats: boolean }; + + let result: { weekday: Weekday; repeats: boolean }; result = yield WeekdayParser; - + weekdays.add(result.weekday); repeats = repeats || result.repeats; - + while (result = yield optional(AnotherWeekdayParser)) { weekdays.add(result.weekday); repeats = repeats || result.repeats; } - + return { weekdays, repeats }; } - + function* MinutesSuffixParser() { - yield ':'; + yield ":"; const minutes = yield ParseInt; return minutes; } - + function* TimeOfDayParser() { let hours = yield ParseInt; const minutes = yield optional(MinutesSuffixParser); - const amOrPm = yield optional('am', 'pm'); - if (amOrPm === 'pm' && hours <= 11) { + const amOrPm = yield optional("am", "pm"); + if (amOrPm === "pm" && hours <= 11) { hours += 12; - } else if (amOrPm === 'am' && hours === 12) { + } else if (amOrPm === "am" && hours === 12) { hours = 24; } return { hours, minutes }; } - + function* TimespanSuffixParser() { - const started = yield optional('to', '-', '–', '—', 'until'); + const started = yield optional("to", "-", "–", "—", "until"); if (started === undefined) return undefined; yield whitespaceOptional; return yield TimeOfDayParser; } - + function* TimespanParser() { - yield ['from', 'at', '']; + yield ["from", "at", ""]; yield whitespaceOptional; const startTime = yield TimeOfDayParser; yield whitespaceOptional; const endTime = yield optional(TimespanSuffixParser); return { startTime, endTime }; } - + interface Result { weekdays: Set; - repeats: undefined | 'weekly'; - startTime: { hours: number, minutes?: number }; - endTime: { hours: number, minutes?: number }; + repeats: undefined | "weekly"; + startTime: { hours: number; minutes?: number }; + endTime: { hours: number; minutes?: number }; } function* NaturalDateParser(): ParseGenerator { yield whitespaceOptional; const { weekdays, repeats } = yield WeekdaysParser; yield whitespaceOptional; - + yield whitespaceOptional; - const timespan = yield optional(TimespanParser); + const timespan = yield optional(TimespanParser); yield whitespaceOptional; - return { repeats: repeats ? 'weekly' : undefined, weekdays, ...(timespan as any) }; + return { + repeats: repeats ? "weekly" : undefined, + weekdays, + ...(timespan as any), + }; } - + function parseNaturalDate(input: string) { input = input.toLowerCase(); - input = input.replace(/[,]/g, ''); + input = input.replace(/[,]/g, ""); return parse(input, NaturalDateParser()); } - test.each([ - ['Monday', { weekdays: new Set(['monday']) }], - ['Wednesday', { weekdays: new Set(['wednesday']) }], - [' Wednesday ', { weekdays: new Set(['wednesday']) }], - ['Wednesday and Saturday', { weekdays: new Set(['wednesday', 'saturday']) }], - ['Wednesday or Saturday', { weekdays: new Set(['wednesday', 'saturday']) }], - ['Wednesday, Saturday', { weekdays: new Set(['wednesday', 'saturday']) }], - ['Wednesday and, Saturday', { weekdays: new Set(['wednesday', 'saturday']) }], - ['Every Wednesday', { repeats: 'weekly', weekdays: new Set(['wednesday']) }], - [' Every Wednesday ', { repeats: 'weekly', weekdays: new Set(['wednesday']) }], - ['Every Wednesday or Saturday', { repeats: 'weekly', weekdays: new Set(['wednesday', 'saturday']) }], - ['Wednesdays', { repeats: 'weekly', weekdays: new Set(['wednesday']) }], - [' Wednesdays ', { repeats: 'weekly', weekdays: new Set(['wednesday']) }], - ['Wednesdays and Tuesdays', { repeats: 'weekly', weekdays: new Set(['wednesday', 'tuesday']) }], - [' Wednesdays and Tuesdays ', { repeats: 'weekly', weekdays: new Set(['wednesday', 'tuesday']) }], - ['Wednesdays and Tuesdays and Fridays and Wednesdays', { repeats: 'weekly', weekdays: new Set(['wednesday', 'tuesday', 'friday']) }], - ['Wednesdays at 9', { repeats: 'weekly', weekdays: new Set(['wednesday']), startTime: { hours: 9 } }], - [' Wednesdays at 9 ', { repeats: 'weekly', weekdays: new Set(['wednesday']), startTime: { hours: 9 } }], - ['Wednesdays at 9:30', { repeats: 'weekly', weekdays: new Set(['wednesday']), startTime: { hours: 9, minutes: 30 } }], - ['Wednesdays at 9:59', { repeats: 'weekly', weekdays: new Set(['wednesday']), startTime: { hours: 9, minutes: 59 } }], - ['Wednesdays at 9:30am', { repeats: 'weekly', weekdays: new Set(['wednesday']), startTime: { hours: 9, minutes: 30 } }], - ['Wednesdays at 9:30pm', { repeats: 'weekly', weekdays: new Set(['wednesday']), startTime: { hours: 21, minutes: 30 } }], - ['Mondays at 11:30', { repeats: 'weekly', weekdays: new Set(['monday']), startTime: { hours: 11, minutes: 30 } }], - ['Mondays at 9:30 to 10:30', { repeats: 'weekly', weekdays: new Set(['monday']), startTime: { hours: 9, minutes: 30 }, endTime: { hours: 10, minutes: 30 } }], - ['Mondays 9:30–10:30', { repeats: 'weekly', weekdays: new Set(['monday']), startTime: { hours: 9, minutes: 30 }, endTime: { hours: 10, minutes: 30 } }], - ['Mondays and Thursdays at 9:30 to 10:30', { repeats: 'weekly', weekdays: new Set(['monday', 'thursday']), startTime: { hours: 9, minutes: 30 }, endTime: { hours: 10, minutes: 30 } }], - ['Mondays at 9:30pm to 10:30pm', { repeats: 'weekly', weekdays: new Set(['monday']), startTime: { hours: 21, minutes: 30 }, endTime: { hours: 22, minutes: 30 } }], - ['Fridays from 11:15am to 12:30pm', { repeats: 'weekly', weekdays: new Set(['friday']), startTime: { hours: 11, minutes: 15 }, endTime: { hours: 12, minutes: 30 } }], - ['Fridays from 11:15am to 12:00am', { repeats: 'weekly', weekdays: new Set(['friday']), startTime: { hours: 11, minutes: 15 }, endTime: { hours: 24, minutes: 0 } }], - ])('%o', (input: string, output) => { + Deno.test.each([ + ["Monday", { weekdays: new Set(["monday"]) }], + ["Wednesday", { weekdays: new Set(["wednesday"]) }], + [" Wednesday ", { weekdays: new Set(["wednesday"]) }], + ["Wednesday and Saturday", { + weekdays: new Set(["wednesday", "saturday"]), + }], + ["Wednesday or Saturday", { weekdays: new Set(["wednesday", "saturday"]) }], + ["Wednesday, Saturday", { weekdays: new Set(["wednesday", "saturday"]) }], + ["Wednesday and, Saturday", { + weekdays: new Set(["wednesday", "saturday"]), + }], + ["Every Wednesday", { + repeats: "weekly", + weekdays: new Set(["wednesday"]), + }], + [" Every Wednesday ", { + repeats: "weekly", + weekdays: new Set(["wednesday"]), + }], + ["Every Wednesday or Saturday", { + repeats: "weekly", + weekdays: new Set(["wednesday", "saturday"]), + }], + ["Wednesdays", { repeats: "weekly", weekdays: new Set(["wednesday"]) }], + [" Wednesdays ", { repeats: "weekly", weekdays: new Set(["wednesday"]) }], + ["Wednesdays and Tuesdays", { + repeats: "weekly", + weekdays: new Set(["wednesday", "tuesday"]), + }], + [" Wednesdays and Tuesdays ", { + repeats: "weekly", + weekdays: new Set(["wednesday", "tuesday"]), + }], + ["Wednesdays and Tuesdays and Fridays and Wednesdays", { + repeats: "weekly", + weekdays: new Set(["wednesday", "tuesday", "friday"]), + }], + ["Wednesdays at 9", { + repeats: "weekly", + weekdays: new Set(["wednesday"]), + startTime: { hours: 9 }, + }], + [" Wednesdays at 9 ", { + repeats: "weekly", + weekdays: new Set(["wednesday"]), + startTime: { hours: 9 }, + }], + ["Wednesdays at 9:30", { + repeats: "weekly", + weekdays: new Set(["wednesday"]), + startTime: { hours: 9, minutes: 30 }, + }], + ["Wednesdays at 9:59", { + repeats: "weekly", + weekdays: new Set(["wednesday"]), + startTime: { hours: 9, minutes: 59 }, + }], + ["Wednesdays at 9:30am", { + repeats: "weekly", + weekdays: new Set(["wednesday"]), + startTime: { hours: 9, minutes: 30 }, + }], + ["Wednesdays at 9:30pm", { + repeats: "weekly", + weekdays: new Set(["wednesday"]), + startTime: { hours: 21, minutes: 30 }, + }], + ["Mondays at 11:30", { + repeats: "weekly", + weekdays: new Set(["monday"]), + startTime: { hours: 11, minutes: 30 }, + }], + ["Mondays at 9:30 to 10:30", { + repeats: "weekly", + weekdays: new Set(["monday"]), + startTime: { hours: 9, minutes: 30 }, + endTime: { hours: 10, minutes: 30 }, + }], + ["Mondays 9:30–10:30", { + repeats: "weekly", + weekdays: new Set(["monday"]), + startTime: { hours: 9, minutes: 30 }, + endTime: { hours: 10, minutes: 30 }, + }], + ["Mondays and Thursdays at 9:30 to 10:30", { + repeats: "weekly", + weekdays: new Set(["monday", "thursday"]), + startTime: { hours: 9, minutes: 30 }, + endTime: { hours: 10, minutes: 30 }, + }], + ["Mondays at 9:30pm to 10:30pm", { + repeats: "weekly", + weekdays: new Set(["monday"]), + startTime: { hours: 21, minutes: 30 }, + endTime: { hours: 22, minutes: 30 }, + }], + ["Fridays from 11:15am to 12:30pm", { + repeats: "weekly", + weekdays: new Set(["friday"]), + startTime: { hours: 11, minutes: 15 }, + endTime: { hours: 12, minutes: 30 }, + }], + ["Fridays from 11:15am to 12:00am", { + repeats: "weekly", + weekdays: new Set(["friday"]), + startTime: { hours: 11, minutes: 15 }, + endTime: { hours: 24, minutes: 0 }, + }], + ])("%o", (input: string, output) => { expect(parseNaturalDate(input)).toEqual({ success: true, result: output, - remaining: '', + remaining: "", }); }); }); diff --git a/src/routing.test.ts b/src/routing.test.ts index 61ee79b..8a72c30 100644 --- a/src/routing.test.ts +++ b/src/routing.test.ts @@ -1,4 +1,5 @@ -import { invert, mustEnd, parse } from "./index"; +import { describe, expect, it } from "./test-deps.ts"; +import { invert, mustEnd, parse } from "./index.ts"; describe("Router", () => { type Route = @@ -160,11 +161,15 @@ describe("Router inversion", () => { }); it("works with single route definition with param", () => { - expect(invert({ type: "album", id: "123" }, AlbumItem())).toEqual("/albums/123"); - expect(invert({ type: "album", id: "678" }, AlbumItem())).toEqual("/albums/678"); + expect(invert({ type: "album", id: "123" }, AlbumItem())).toEqual( + "/albums/123", + ); + expect(invert({ type: "album", id: "678" }, AlbumItem())).toEqual( + "/albums/678", + ); expect(invert({ type: "album", id: "abc" }, AlbumItem())).toBeNull(); expect(invert({ type: "BLAH", id: "123" }, AlbumItem())).toBeNull(); - }) + }); it("works with nested routes", () => { expect(invert({ type: "home" }, Routes())).toEqual("/"); @@ -175,17 +180,20 @@ describe("Router inversion", () => { it("works with routes with nested prefix", () => { expect(invert({ type: "blog" }, BlogHome())).toEqual("/blog"); - expect(invert({ type: "blogArticle", slug: "hello-world" }, BlogArticle())).toEqual("/blog/hello-world"); + expect(invert({ type: "blogArticle", slug: "hello-world" }, BlogArticle())) + .toEqual("/blog/hello-world"); expect(invert({ type: "blog" }, BlogRoutes())).toEqual("/blog"); - expect(invert({ type: "blogArticle", slug: "hello-world" }, BlogRoutes())).toEqual("/blog/hello-world"); + expect(invert({ type: "blogArticle", slug: "hello-world" }, BlogRoutes())) + .toEqual("/blog/hello-world"); expect(invert({ type: "BLAH" }, BlogRoutes())).toBeNull(); }); it("all works with double nested routes", () => { expect(invert({ type: "home" }, DoubleNested())).toEqual("/"); expect(invert({ type: "blog" }, DoubleNested())).toEqual("/blog"); - expect(invert({ type: "blogArticle", slug: "hello-world" }, DoubleNested())).toEqual("/blog/hello-world"); + expect(invert({ type: "blogArticle", slug: "hello-world" }, DoubleNested())) + .toEqual("/blog/hello-world"); expect(invert({ type: "BLAH" }, DoubleNested())).toBeNull(); }); }); diff --git a/src/tailwindcss.test.ts b/src/tailwindcss.test.ts index 86df8f7..74ce580 100644 --- a/src/tailwindcss.test.ts +++ b/src/tailwindcss.test.ts @@ -1,11 +1,14 @@ -let tailwindExcerpt = `/*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */*,::after,::before{box-sizing:border-box}:root{-moz-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset{margin:0;padding:0}ol,ul{list-style:none;margin:0;padding:0}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";line-height:1.5}body{font-family:inherit;line-height:inherit}*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::placeholder,textarea::placeholder{color:#9ca3af}[role=button],button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px * var(--tw-space-y-reverse))}.space-x-0>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0px * var(--tw-space-x-reverse));margin-left:calc(0px * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem * var(--tw-space-x-reverse));margin-left:calc(.75rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}`; +import { describe, expect, it } from "./test-deps.ts"; + +let tailwindExcerpt = + `/*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */*,::after,::before{box-sizing:border-box}:root{-moz-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}button{background-color:transparent;background-image:none}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}fieldset{margin:0;padding:0}ol,ul{list-style:none;margin:0;padding:0}html{font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";line-height:1.5}body{font-family:inherit;line-height:inherit}*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}hr{border-top-width:1px}img{border-style:solid}textarea{resize:vertical}input::placeholder,textarea::placeholder{color:#9ca3af}[role=button],button{cursor:pointer}table{border-collapse:collapse}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}button,input,optgroup,select,textarea{padding:0;line-height:inherit;color:inherit}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px * var(--tw-space-y-reverse))}.space-x-0>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(0px * var(--tw-space-x-reverse));margin-left:calc(0px * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem * var(--tw-space-x-reverse));margin-left:calc(.25rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem * var(--tw-space-x-reverse));margin-left:calc(.75rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}`; // tailwindExcerpt = `/*! modern-normalize v1.0.0 | MIT License | https://github.com/sindresorhus/modern-normalize */*,::after,::before{box-sizing:border-box}:root{-moz-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji'}hr{height:0;color:inherit}`; -import { parse, hasMore, has, lookAhead, ParseGenerator } from './index'; +import { has, hasMore, lookAhead, parse, ParseGenerator } from "./index.ts"; interface CSSComment { - type: 'comment'; + type: "comment"; content: string; } @@ -15,13 +18,13 @@ interface CSSDeclaration { } interface CSSRule { - type: 'rule'; + type: "rule"; selectors: Array; declarations: Array; } interface CSSMedia { - type: 'media'; + type: "media"; rawFeatures: string; rules: Array; } @@ -44,11 +47,11 @@ function* ValueParser() { function* DeclarationParser() { const name = yield PropertyParser; yield whitespaceMay; - yield ':'; + yield ":"; yield whitespaceMay; const rawValue = yield ValueParser; yield whitespaceMay; - yield has(';'); + yield has(";"); return { name, rawValue }; } @@ -70,36 +73,43 @@ function* RuleParser(): ParseGenerator { while (true) { selectors.push(yield SelectorComponentParser); yield whitespaceMay; - if (yield has(',')) { + if (yield has(",")) { yield whitespaceMay; continue; } - if (yield has('{')) break; + if (yield has("{")) break; } // yield whitespaceMay; // yield "{"; yield whitespaceMay; - while ((yield has('}')) === false) { + while ((yield has("}")) === false) { declarations.push(yield DeclarationParser); yield whitespaceMay; } - return { type: 'rule', selectors: selectors, declarations }; + return { type: "rule", selectors: selectors, declarations }; } -function* MediaQueryParser() { - yield '@media'; +type MediaQueryParser = { + type: "media"; + rawFeatures: string; + rules: ReadonlyArray; +}; +function* MediaQueryParser(): + | Generator> + | Generator { + yield "@media"; yield whitespaceMay; - yield '('; + yield "("; const [rawFeatures]: [string] = yield /^[^)]+/; - yield ')'; + yield ")"; yield whitespaceMay; - yield '{'; - const rules = yield RulesParser; - yield '}'; - return { type: 'media', rawFeatures, rules }; + yield "{"; + const rules: ReadonlyArray = yield RulesParser; + yield "}"; + return { type: "media", rawFeatures, rules }; } function* CommentParser(): Generator< @@ -107,9 +117,9 @@ function* CommentParser(): Generator< CSSComment, [string, string] > { - yield '/*'; + yield "/*"; const [, content] = yield /^(.*?)\*\//; - return { type: 'comment', content }; + return { type: "comment", content }; } function* RulesParser(): ParseGenerator> { @@ -146,199 +156,200 @@ function parseCSS(cssSource: string) { ///// function* generateComment(item: CSSComment) { - yield '/*'; + yield "/*"; yield item.content; - yield '*/'; + yield "*/"; } function* generateDeclaration(declaration: CSSDeclaration) { yield declaration.name; - yield ':'; + yield ":"; yield declaration.rawValue; } function* generateRule(item: CSSRule) { - yield item.selectors.join(','); - yield '{'; + yield item.selectors.join(","); + yield "{"; for (const [index, declaration] of item.declarations.entries()) { yield* generateDeclaration(declaration); if (index < item.declarations.length - 1) { - yield ';'; + yield ";"; } } - yield '}'; + yield "}"; } function* generateMediaSource(item: CSSMedia) { - yield '@media '; - yield '('; + yield "@media "; + yield "("; yield item.rawFeatures; - yield ')'; - yield '{'; + yield ")"; + yield "{"; for (const rule of item.rules) { yield* generateRule(rule); } - yield '}'; + yield "}"; } function* generateCSSSource(items: Array) { for (const item of items) { - if (item.type === 'media') { + if (item.type === "media") { yield* generateMediaSource(item); - } else if (item.type === 'rule') { + } else if (item.type === "rule") { yield* generateRule(item); - } else if (item.type === 'comment') { + } else if (item.type === "comment") { yield* generateComment(item); } } } function stringifyCSS(items: Array) { - return Array.from(generateCSSSource(items)).join(''); + return Array.from(generateCSSSource(items)).join(""); } -describe('CSS values', () => { - it('parses 42', () => { - expect(parse('42', ValueParser())).toMatchObject({ - remaining: '', - result: '42', +describe("CSS values", () => { + it("parses 42", () => { + expect(parse("42", ValueParser())).toMatchObject({ + remaining: "", + result: "42", success: true, }); }); - it('parses 1.15', () => { - expect(parse('1.15', ValueParser())).toMatchObject({ - remaining: '', - result: '1.15', + it("parses 1.15", () => { + expect(parse("1.15", ValueParser())).toMatchObject({ + remaining: "", + result: "1.15", success: true, }); }); - it('parses 1.', () => { - expect(parse('1.', ValueParser())).toMatchObject({ - remaining: '', - result: '1.', + it("parses 1.", () => { + expect(parse("1.", ValueParser())).toMatchObject({ + remaining: "", + result: "1.", success: true, }); }); - it('parses .1', () => { - expect(parse('.1', ValueParser())).toMatchObject({ - remaining: '', - result: '.1', + it("parses .1", () => { + expect(parse(".1", ValueParser())).toMatchObject({ + remaining: "", + result: ".1", success: true, }); }); - it('parses hex color', () => { - expect(parse('#e5e7eb', ValueParser())).toMatchObject({ - remaining: '', - result: '#e5e7eb', + it("parses hex color", () => { + expect(parse("#e5e7eb", ValueParser())).toMatchObject({ + remaining: "", + result: "#e5e7eb", success: true, }); }); - it('parses 100%', () => { - expect(parse('100%', ValueParser())).toMatchObject({ - remaining: '', - result: '100%', + it("parses 100%", () => { + expect(parse("100%", ValueParser())).toMatchObject({ + remaining: "", + result: "100%", success: true, }); }); - it('parses font stack', () => { + it("parses font stack", () => { expect( parse( `system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji'`, - ValueParser() - ) + ValueParser(), + ), ).toMatchObject({ - remaining: '', - result: `system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji'`, + remaining: "", + result: + `system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji'`, success: true, }); }); - it('parses border long-form', () => { + it("parses border long-form", () => { expect(parse(`1px solid black`, ValueParser())).toMatchObject({ - remaining: '', + remaining: "", result: `1px solid black`, success: true, }); }); - it('parses var', () => { + it("parses var", () => { expect(parse(`var(--primary)`, ValueParser())).toMatchObject({ - remaining: '', + remaining: "", result: `var(--primary)`, success: true, }); }); }); -describe('selectors', () => { - it('parses .container', () => { - expect(parse('.container', SelectorComponentParser())).toEqual({ - remaining: '', - result: '.container', +describe("selectors", () => { + it("parses .container", () => { + expect(parse(".container", SelectorComponentParser())).toEqual({ + remaining: "", + result: ".container", success: true, }); }); }); -describe('media queries', () => { - it('parses empty', () => { +describe("media queries", () => { + it("parses empty", () => { expect( - parse(`@media (min-width:640px){}`, MediaQueryParser() as any) + parse(`@media (min-width:640px){}`, MediaQueryParser() as any), ).toEqual({ - remaining: '', + remaining: "", result: { - rawFeatures: 'min-width:640px', + rawFeatures: "min-width:640px", rules: [], - type: 'media', + type: "media", }, success: true, }); }); - it('parses with class', () => { + it("parses with class", () => { expect( parse( `@media (min-width:640px){.container{max-width:640px}}`, - MediaQueryParser() as any - ) + MediaQueryParser() as any, + ), ).toEqual({ - remaining: '', + remaining: "", result: { - rawFeatures: 'min-width:640px', + rawFeatures: "min-width:640px", rules: [ { declarations: [ { - name: 'max-width', - rawValue: '640px', + name: "max-width", + rawValue: "640px", }, ], - selectors: ['.container'], - type: 'rule', + selectors: [".container"], + type: "rule", }, ], - type: 'media', + type: "media", }, success: true, }); }); }); -it('parses Tailwind excerpt', () => { +it("parses Tailwind excerpt", () => { const result = parseCSS(tailwindExcerpt); expect(result.success).toBe(true); }); -it('parses and stringifies Tailwind excerpt', () => { +it("parses and stringifies Tailwind excerpt", () => { const result = parseCSS(tailwindExcerpt); if (result.success !== true) { - fail('Parsing failed'); + fail("Parsing failed"); } expect(stringifyCSS(result.result)).toEqual(tailwindExcerpt); diff --git a/src/test-deps.ts b/src/test-deps.ts new file mode 100644 index 0000000..68a6582 --- /dev/null +++ b/src/test-deps.ts @@ -0,0 +1,7 @@ +export { expect } from "https://deno.land/x/expect/mod.ts"; +export { + afterEach, + beforeEach, + describe, + it, +} from "https://deno.land/std@0.207.0/testing/bdd.ts"; diff --git a/tsconfig.json b/tsconfig.json index e91610e..65677a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,5 +33,6 @@ "forceConsistentCasingInFileNames": true, // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` "noEmit": true, + "allowImportingTsExtensions": true } }