From 2c7e152644088ff49fa2cd0538a3cdec6dda917e Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Thu, 13 Jun 2024 11:19:25 +0900 Subject: [PATCH 1/3] fix lints and kill any (#142) * lint: fix lint warning that is easy to fix * lint: typesafe signature of seqOrText * lint: typesafe createLanguage and language * lint: typesafe seq * lint: typesafe Parser.option * fix: node can be string * lint: typesafe alt * fix: invalid url in link element will cause error * chore: get rid of any * fix: unnecessary import * lint: kill any but still with loose type checking * Revert "lint: kill any but still with loose type checking" This reverts commit 8c7462f4a745800499a63ecf0632df3647b3e22c. * lint: kill any again * test: write type test * ci: upgrade node version for lint --- .eslintrc.js | 11 +- .github/workflows/api.yml | 2 +- .github/workflows/lint.yml | 2 +- .github/workflows/test.yml | 2 +- src/cli/parse.ts | 2 +- src/cli/parseSimple.ts | 2 +- src/internal/core/index.ts | 66 +++++--- src/internal/index.ts | 10 +- src/internal/parser.ts | 325 +++++++++++++++++++++---------------- src/internal/util.ts | 10 +- test-d/index.ts | 16 ++ test/parser.ts | 8 + 12 files changed, 281 insertions(+), 175 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 92d542f..4f9858b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -43,14 +43,21 @@ module.exports = { 'prefer-arrow-callback': ['error'], 'no-throw-literal': ['error'], 'no-param-reassign': ['warn'], - 'no-constant-condition': ['warn'], + 'no-constant-condition': ['warn', { + checkLoops: false, + }], 'no-empty-pattern': ['warn'], - '@typescript-eslint/no-unnecessary-condition': ['warn'], + '@typescript-eslint/no-unnecessary-condition': ['warn', { + allowConstantLoopConditions: true, + }], '@typescript-eslint/no-inferrable-types': ['warn'], '@typescript-eslint/no-non-null-assertion': ['warn'], '@typescript-eslint/explicit-function-return-type': ['warn'], '@typescript-eslint/no-misused-promises': ['error', { 'checksVoidReturn': false, }], + '@typescript-eslint/no-unused-vars': ['error', { + "argsIgnorePattern": "^_", + }] }, }; diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index dcbc7c0..7364074 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -14,7 +14,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v1 with: - node-version: 16.5.0 + node-version: 16.10.0 - name: Cache dependencies uses: actions/cache@v2 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cdf6d20..dea9d5a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -14,7 +14,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v1 with: - node-version: 16.5.0 + node-version: 16.10.0 - name: Cache dependencies uses: actions/cache@v2 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e072e19..598042b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,7 @@ jobs: strategy: matrix: - node-version: [16.5.0] + node-version: [16.10.0] steps: - name: Checkout diff --git a/src/cli/parse.ts b/src/cli/parse.ts index 120df08..b37e7ae 100644 --- a/src/cli/parse.ts +++ b/src/cli/parse.ts @@ -2,7 +2,7 @@ import { performance } from 'perf_hooks'; import inputLine, { InputCanceledError } from './misc/inputLine'; import { parse } from '..'; -async function entryPoint() { +async function entryPoint(): Promise { console.log('intaractive parser'); while (true) { diff --git a/src/cli/parseSimple.ts b/src/cli/parseSimple.ts index 41674b4..e01b299 100644 --- a/src/cli/parseSimple.ts +++ b/src/cli/parseSimple.ts @@ -2,7 +2,7 @@ import { performance } from 'perf_hooks'; import inputLine, { InputCanceledError } from './misc/inputLine'; import { parseSimple } from '..'; -async function entryPoint() { +async function entryPoint(): Promise { console.log('intaractive simple parser'); while (true) { diff --git a/src/internal/core/index.ts b/src/internal/core/index.ts index 2241d50..b9c21a1 100644 --- a/src/internal/core/index.ts +++ b/src/internal/core/index.ts @@ -12,7 +12,14 @@ export type Failure = { success: false }; export type Result = Success | Failure; -export type ParserHandler = (input: string, index: number, state: any) => Result +interface State { + trace?: boolean, + linkLabel?: boolean, + nestLimit: number, + depth: number, +} + +export type ParserHandler = (input: string, index: number, state: State) => Result export function success(index: number, value: T): Success { return { @@ -31,7 +38,7 @@ export class Parser { public handler: ParserHandler; constructor(handler: ParserHandler, name?: string) { - this.handler = (input, index, state) => { + this.handler = (input, index, state) : Failure | Success => { if (state.trace && this.name != null) { const pos = `${index}`; console.log(`${pos.padEnd(6, ' ')}enter ${this.name}`); @@ -91,20 +98,24 @@ export class Parser { }); } - sep(separator: Parser, min: number): Parser { + sep(separator: Parser, min: number): Parser { if (min < 1) { throw new Error('"min" must be a value greater than or equal to 1.'); } - return seq([ + return seq( this, - seq([ + seq( separator, this, - ], 1).many(min - 1), - ]).map(result => [result[0], ...result[1]]); + ).select(1).many(min - 1), + ).map(result => [result[0], ...result[1]]); } - option(): Parser { + select(key: K): Parser { + return this.map(v => v[key]); + } + + option(): Parser { return alt([ this, succeeded(null), @@ -136,7 +147,17 @@ export function regexp(pattern: T): Parser { }); } -export function seq(parsers: Parser[], select?: number): Parser { +type ParsedType> = T extends Parser ? U : never; + +export type SeqParseResult = + T extends [] ? [] + : T extends [infer F, ...infer R] + ? ( + F extends Parser ? [ParsedType, ...SeqParseResult] : [unknown, ...SeqParseResult] + ) + : unknown[]; + +export function seq[]>(...parsers: Parsers): Parser> { return new Parser((input, index, state) => { let result; let latestIndex = index; @@ -149,17 +170,17 @@ export function seq(parsers: Parser[], select?: number): Parser { latestIndex = result.index; accum.push(result.value); } - return success(latestIndex, (select != null ? accum[select] : accum)); + return success(latestIndex, accum as SeqParseResult); }); } -export function alt(parsers: Parser[]): Parser { - return new Parser((input, index, state) => { - let result; +export function alt[]>(parsers: Parsers): Parser> { + return new Parser>((input, index, state): Result> => { for (let i = 0; i < parsers.length; i++) { - result = parsers[i].handler(input, index, state); + const parser: Parsers[number] = parsers[i]; + const result = parser.handler(input, index, state); if (result.success) { - return result; + return result as Result>; } } return failure(); @@ -172,7 +193,7 @@ function succeeded(value: T): Parser { }); } -export function notMatch(parser: Parser): Parser { +export function notMatch(parser: Parser): Parser { return new Parser((input, index, state) => { const result = parser.handler(input, index, state); return !result.success @@ -232,12 +253,15 @@ export function lazy(fn: () => Parser): Parser { //type SyntaxReturn = T extends (rules: Record>) => infer R ? R : never; //export function createLanguage2>>(syntaxes: T): { [K in keyof T]: SyntaxReturn } { +type ParserTable = { [K in keyof T]: Parser }; + // TODO: 関数の型宣言をいい感じにしたい -export function createLanguage(syntaxes: { [K in keyof T]: (r: Record>) => T[K] }): T { - const rules: Record> = {}; - for (const key of Object.keys(syntaxes)) { +export function createLanguage(syntaxes: { [K in keyof T]: (r: ParserTable) => Parser }): ParserTable { + // @ts-expect-error initializing object so type error here + const rules: ParserTable = {}; + for (const key of Object.keys(syntaxes) as (keyof T & string)[]) { rules[key] = lazy(() => { - const parser = (syntaxes as any)[key](rules); + const parser = syntaxes[key](rules); if (parser == null) { throw new Error('syntax must return a parser.'); } @@ -245,5 +269,5 @@ export function createLanguage(syntaxes: { [K in keyof T]: (r: Record; + }); + if (!result.success) throw new Error('Unexpected parse error'); return mergeText(result.value); } export function simpleParser(input: string): M.MfmSimpleNode[] { - const result = language.simpleParser.handler(input, 0, { }) as P.Success; + const result = language.simpleParser.handler(input, 0, { + depth: 0, + nestLimit: 1 / 0, // reliable infinite + }); + if (!result.success) throw new Error('Unexpected parse error'); return mergeText(result.value); } diff --git a/src/internal/parser.ts b/src/internal/parser.ts index 20e0072..d64b122 100644 --- a/src/internal/parser.ts +++ b/src/internal/parser.ts @@ -1,6 +1,7 @@ import * as M from '..'; import * as P from './core'; import { mergeText } from './util'; +import { SeqParseResult } from './core'; // NOTE: // tsdのテストでファイルを追加しているにも関わらず「@twemoji/parser/dist/lib/regex」の型定義ファイルがないとエラーが出るため、 @@ -16,9 +17,10 @@ const space = P.regexp(/[\u0020\u3000\t]/); const alphaAndNum = P.regexp(/[a-z0-9]/i); const newLine = P.alt([P.crlf, P.cr, P.lf]); -function seqOrText(parsers: P.Parser[]): P.Parser { - return new P.Parser((input, index, state) => { - const accum: any[] = []; +function seqOrText[]>(...parsers: Parsers): P.Parser | string> { + return new P.Parser | string>((input, index, state) => { + // TODO: typesafe implementation + const accum: unknown[] = []; let latestIndex = index; for (let i = 0 ; i < parsers.length; i++) { const result = parsers[i].handler(input, latestIndex, state); @@ -32,7 +34,7 @@ function seqOrText(parsers: P.Parser[]): P.Parser { accum.push(result.value); latestIndex = result.index; } - return P.success(latestIndex, accum); + return P.success(latestIndex, accum as SeqParseResult); }); } @@ -51,7 +53,7 @@ const nestable = new P.Parser((_input, index, state) => { function nest(parser: P.Parser, fallback?: P.Parser): P.Parser { // nesting limited? -> No: specified parser, Yes: fallback parser (default = P.char) const inner = P.alt([ - P.seq([nestable, parser], 1), + P.seq(nestable, parser).select(1), (fallback != null) ? fallback : P.char, ]); return new P.Parser((input, index, state) => { @@ -62,7 +64,42 @@ function nest(parser: P.Parser, fallback?: P.Parser): P.Parser, + codeBlock: M.NodeType<'blockCode'>, + mathBlock: M.NodeType<'mathBlock'>, + centerTag: M.NodeType<'center'>, + big: M.NodeType<'fn'> | string, + boldAsta: M.NodeType<'bold'> | string, + boldTag: M.NodeType<'bold'> | string, + boldUnder: M.NodeType<'bold'>, + smallTag: M.NodeType<'small'> | string, + italicTag: M.NodeType<'italic'> | string, + italicAsta: M.NodeType<'italic'>, + italicUnder: M.NodeType<'italic'>, + strikeTag: M.NodeType<'strike'> | string, + strikeWave: M.NodeType<'strike'> | string, + unicodeEmoji: M.NodeType<'unicodeEmoji'>, + plainTag: M.NodeType<'plain'>, + fn: M.NodeType<'fn'> | string, + inlineCode: M.NodeType<'inlineCode'>, + mathInline: M.NodeType<'mathInline'>, + mention: M.NodeType<'mention'> | string, + hashtag: M.NodeType<'hashtag'>, + emojiCode: M.NodeType<'emojiCode'>, + link: M.NodeType<'link'>, + url: M.NodeType<'url'> | string, + urlAlt: M.NodeType<'url'>, + search: M.NodeType<'search'>, + text: string, +} + +export const language = P.createLanguage({ fullParser: r => { return r.full.many(0); }, @@ -186,19 +223,19 @@ export const language = P.createLanguage({ }, quote: r => { - const lines: P.Parser = P.seq([ + const lines: P.Parser = P.seq( P.str('>'), space.option(), - P.seq([P.notMatch(newLine), P.char], 1).many(0).text(), - ], 2).sep(newLine, 1); - const parser = P.seq([ + P.seq(P.notMatch(newLine), P.char).select(1).many(0).text(), + ).select(2).sep(newLine, 1); + const parser = P.seq( newLine.option(), newLine.option(), P.lineBegin, lines, newLine.option(), newLine.option(), - ], 3); + ).select(3); return new P.Parser((input, index, state) => { let result; // parse quote @@ -222,41 +259,41 @@ export const language = P.createLanguage({ }); }, - codeBlock: r => { + codeBlock: () => { const mark = P.str('```'); - return P.seq([ + return P.seq( newLine.option(), P.lineBegin, mark, - P.seq([P.notMatch(newLine), P.char], 1).many(0), + P.seq(P.notMatch(newLine), P.char).select(1).many(0), newLine, - P.seq([P.notMatch(P.seq([newLine, mark, P.lineEnd])), P.char], 1).many(1), + P.seq(P.notMatch(P.seq(newLine, mark, P.lineEnd)), P.char).select(1).many(1), newLine, mark, P.lineEnd, newLine.option(), - ]).map(result => { - const lang = (result[3] as string[]).join('').trim(); - const code = (result[5] as string[]).join(''); + ).map(result => { + const lang = result[3].join('').trim(); + const code = result[5].join(''); return M.CODE_BLOCK(code, (lang.length > 0 ? lang : null)); }); }, - mathBlock: r => { + mathBlock: () => { const open = P.str('\\['); const close = P.str('\\]'); - return P.seq([ + return P.seq( newLine.option(), P.lineBegin, open, newLine.option(), - P.seq([P.notMatch(P.seq([newLine.option(), close])), P.char], 1).many(1), + P.seq(P.notMatch(P.seq(newLine.option(), close)), P.char).select(1).many(1), newLine.option(), close, P.lineEnd, newLine.option(), - ]).map(result => { - const formula = (result[4] as string[]).join(''); + ).map(result => { + const formula = result[4].join(''); return M.MATH_BLOCK(formula); }); }, @@ -264,28 +301,28 @@ export const language = P.createLanguage({ centerTag: r => { const open = P.str('
'); const close = P.str('
'); - return P.seq([ + return P.seq( newLine.option(), P.lineBegin, open, newLine.option(), - P.seq([P.notMatch(P.seq([newLine.option(), close])), nest(r.inline)], 1).many(1), + P.seq(P.notMatch(P.seq(newLine.option(), close)), nest(r.inline)).select(1).many(1), newLine.option(), close, P.lineEnd, newLine.option(), - ]).map(result => { + ).map(result => { return M.CENTER(mergeText(result[4])); }); }, big: r => { const mark = P.str('***'); - return seqOrText([ + return seqOrText( mark, - P.seq([P.notMatch(mark), nest(r.inline)], 1).many(1), + P.seq(P.notMatch(mark), nest(r.inline)).select(1).many(1), mark, - ]).map(result => { + ).map(result => { if (typeof result === 'string') return result; return M.FN('tada', {}, mergeText(result[1])); }); @@ -293,71 +330,71 @@ export const language = P.createLanguage({ boldAsta: r => { const mark = P.str('**'); - return seqOrText([ + return seqOrText( mark, - P.seq([P.notMatch(mark), nest(r.inline)], 1).many(1), + P.seq(P.notMatch(mark), nest(r.inline)).select(1).many(1), mark, - ]).map(result => { + ).map(result => { if (typeof result === 'string') return result; - return M.BOLD(mergeText(result[1] as (M.MfmInline | string)[])); + return M.BOLD(mergeText(result[1])); }); }, boldTag: r => { const open = P.str(''); const close = P.str(''); - return seqOrText([ + return seqOrText( open, - P.seq([P.notMatch(close), nest(r.inline)], 1).many(1), + P.seq(P.notMatch(close), nest(r.inline)).select(1).many(1), close, - ]).map(result => { + ).map(result => { if (typeof result === 'string') return result; - return M.BOLD(mergeText(result[1] as (M.MfmInline | string)[])); + return M.BOLD(mergeText(result[1])); }); }, - boldUnder: r => { + boldUnder: () => { const mark = P.str('__'); - return P.seq([ + return P.seq( mark, P.alt([alphaAndNum, space]).many(1), mark, - ]).map(result => M.BOLD(mergeText(result[1] as string[]))); + ).map(result => M.BOLD(mergeText(result[1]))); }, smallTag: r => { const open = P.str(''); const close = P.str(''); - return seqOrText([ + return seqOrText( open, - P.seq([P.notMatch(close), nest(r.inline)], 1).many(1), + P.seq(P.notMatch(close), nest(r.inline)).select(1).many(1), close, - ]).map(result => { + ).map(result => { if (typeof result === 'string') return result; - return M.SMALL(mergeText(result[1] as (M.MfmInline | string)[])); + return M.SMALL(mergeText(result[1])); }); }, italicTag: r => { const open = P.str(''); const close = P.str(''); - return seqOrText([ + return seqOrText( open, - P.seq([P.notMatch(close), nest(r.inline)], 1).many(1), + P.seq(P.notMatch(close), nest(r.inline)).select(1).many(1), close, - ]).map(result => { + ).map(result => { if (typeof result === 'string') return result; - return M.ITALIC(mergeText(result[1] as (M.MfmInline | string)[])); + return M.ITALIC(mergeText(result[1])); }); }, - italicAsta: r => { + italicAsta: () => { const mark = P.str('*'); - const parser = P.seq([ + const parser = P.seq( mark, P.alt([alphaAndNum, space]).many(1), mark, - ]); + ); return new P.Parser((input, index, state) => { const result = parser.handler(input, index, state); if (!result.success) { @@ -368,17 +405,17 @@ export const language = P.createLanguage({ if (/[a-z0-9]$/i.test(beforeStr)) { return P.failure(); } - return P.success(result.index, M.ITALIC(mergeText(result.value[1] as string[]))); + return P.success(result.index, M.ITALIC(mergeText(result.value[1]))); }); }, - italicUnder: r => { + italicUnder: () => { const mark = P.str('_'); - const parser = P.seq([ + const parser = P.seq( mark, P.alt([alphaAndNum, space]).many(1), mark, - ]); + ); return new P.Parser((input, index, state) => { const result = parser.handler(input, index, state); if (!result.success) { @@ -389,53 +426,53 @@ export const language = P.createLanguage({ if (/[a-z0-9]$/i.test(beforeStr)) { return P.failure(); } - return P.success(result.index, M.ITALIC(mergeText(result.value[1] as string[]))); + return P.success(result.index, M.ITALIC(mergeText(result.value[1]))); }); }, strikeTag: r => { const open = P.str(''); const close = P.str(''); - return seqOrText([ + return seqOrText( open, - P.seq([P.notMatch(close), nest(r.inline)], 1).many(1), + P.seq(P.notMatch(close), nest(r.inline)).select(1).many(1), close, - ]).map(result => { + ).map(result => { if (typeof result === 'string') return result; - return M.STRIKE(mergeText(result[1] as (M.MfmInline | string)[])); + return M.STRIKE(mergeText(result[1])); }); }, strikeWave: r => { const mark = P.str('~~'); - return seqOrText([ + return seqOrText( mark, - P.seq([P.notMatch(P.alt([mark, newLine])), nest(r.inline)], 1).many(1), + P.seq(P.notMatch(P.alt([mark, newLine])), nest(r.inline)).select(1).many(1), mark, - ]).map(result => { + ).map(result => { if (typeof result === 'string') return result; - return M.STRIKE(mergeText(result[1] as (M.MfmInline | string)[])); + return M.STRIKE(mergeText(result[1])); }); }, - unicodeEmoji: r => { + unicodeEmoji: () => { const emoji = RegExp(twemojiRegex.source); return P.regexp(emoji).map(content => M.UNI_EMOJI(content)); }, - plainTag: r => { + plainTag: () => { const open = P.str(''); const close = P.str(''); - return P.seq([ + return P.seq( open, newLine.option(), - P.seq([ - P.notMatch(P.seq([newLine.option(), close])), + P.seq( + P.notMatch(P.seq(newLine.option(), close)), P.char, - ], 1).many(1).text(), + ).select(1).many(1).text(), newLine.option(), close, - ], 2).map(result => M.PLAIN(result)); + ).select(2).map(result => M.PLAIN(result)); }, fn: r => { @@ -446,22 +483,22 @@ export const language = P.createLanguage({ } return P.success(result.index, result.value); }); - const arg: P.Parser = P.seq([ + const arg: P.Parser = P.seq( P.regexp(/[a-z0-9_]+/i), - P.seq([ + P.seq( P.str('='), P.regexp(/[a-z0-9_.-]+/i), - ], 1).option(), - ]).map(result => { + ).select(1).option(), + ).map(result => { return { k: result[0], v: (result[1] != null) ? result[1] : true, }; }); - const args = P.seq([ + const args = P.seq( P.str('.'), arg.sep(P.str(','), 1), - ], 1).map(pairs => { + ).select(1).map(pairs => { const result: Args = { }; for (const pair of pairs) { result[pair.k] = pair.v; @@ -469,57 +506,57 @@ export const language = P.createLanguage({ return result; }); const fnClose = P.str(']'); - return seqOrText([ + return seqOrText( P.str('$['), fnName, args.option(), P.str(' '), - P.seq([P.notMatch(fnClose), nest(r.inline)], 1).many(1), + P.seq(P.notMatch(fnClose), nest(r.inline)).select(1).many(1), fnClose, - ]).map(result => { + ).map(result => { if (typeof result === 'string') return result; const name = result[1]; - const args = result[2] || {}; + const args: Args = result[2] || {}; const content = result[4]; return M.FN(name, args, mergeText(content)); }); }, - inlineCode: r => { + inlineCode: () => { const mark = P.str('`'); - return P.seq([ + return P.seq( mark, - P.seq([ + P.seq( P.notMatch(P.alt([mark, P.str('´'), newLine])), P.char, - ], 1).many(1), + ).select(1).many(1), mark, - ]).map(result => M.INLINE_CODE(result[1].join(''))); + ).map(result => M.INLINE_CODE(result[1].join(''))); }, - mathInline: r => { + mathInline: () => { const open = P.str('\\('); const close = P.str('\\)'); - return P.seq([ + return P.seq( open, - P.seq([ + P.seq( P.notMatch(P.alt([close, newLine])), P.char, - ], 1).many(1), + ).select(1).many(1), close, - ]).map(result => M.MATH_INLINE(result[1].join(''))); + ).map(result => M.MATH_INLINE(result[1].join(''))); }, - mention: r => { - const parser = P.seq([ + mention: () => { + const parser = P.seq( notLinkLabel, P.str('@'), P.regexp(/[a-z0-9_-]+/i), - P.seq([ + P.seq( P.str('@'), P.regexp(/[a-z0-9_.-]+/i), - ], 1).option(), - ]); + ).select(1).option(), + ); return new P.Parser((input, index, state) => { let result; result = parser.handler(input, index, state); @@ -576,32 +613,32 @@ export const language = P.createLanguage({ }); }, - hashtag: r => { + hashtag: () => { const mark = P.str('#'); - const hashTagChar = P.seq([ + const hashTagChar = P.seq( P.notMatch(P.alt([P.regexp(/[ \u3000\t.,!?'"#:/[\]【】()「」()<>]/), space, newLine])), P.char, - ], 1); - const innerItem: P.Parser = P.lazy(() => P.alt([ - P.seq([ + ).select(1); + const innerItem: P.Parser = P.lazy(() => P.alt([ + P.seq( P.str('('), nest(innerItem, hashTagChar).many(0), P.str(')'), - ]), - P.seq([ + ), + P.seq( P.str('['), nest(innerItem, hashTagChar).many(0), P.str(']'), - ]), - P.seq([ + ), + P.seq( P.str('「'), nest(innerItem, hashTagChar).many(0), P.str('」'), - ]), - P.seq([ + ), + P.seq( P.str('('), nest(innerItem, hashTagChar).many(0), P.str(')'), - ]), + ), hashTagChar, ])); - const parser = P.seq([ + const parser = P.seq( notLinkLabel, mark, innerItem.many(1).text(), - ], 2); + ).select(2); return new P.Parser((input, index, state) => { const result = parser.handler(input, index, state); if (!result.success) { @@ -622,16 +659,16 @@ export const language = P.createLanguage({ }); }, - emojiCode: r => { + emojiCode: () => { const side = P.notMatch(P.regexp(/[a-z0-9]/i)); const mark = P.str(':'); - return P.seq([ + return P.seq( P.alt([P.lineBegin, side]), mark, P.regexp(/[a-z0-9_+-]+/i), mark, P.alt([P.lineEnd, side]), - ], 2).map(name => M.EMOJI_CODE(name as string)); + ).select(2).map(name => M.EMOJI_CODE(name)); }, link: r => { @@ -642,41 +679,49 @@ export const language = P.createLanguage({ return result; }); const closeLabel = P.str(']'); - return P.seq([ + const parser = P.seq( notLinkLabel, P.alt([P.str('?['), P.str('[')]), - P.seq([ + P.seq( P.notMatch(P.alt([closeLabel, newLine])), nest(labelInline), - ], 1).many(1), + ).select(1).many(1), closeLabel, P.str('('), P.alt([r.urlAlt, r.url]), P.str(')'), - ]).map(result => { - const silent = (result[1] === '?['); - const label = result[2]; - const url: M.MfmUrl = result[5]; - return M.LINK(silent, url.props.url, mergeText(label)); + ); + return new P.Parser((input, index, state) => { + const result = parser.handler(input, index, state); + if (!result.success) { + return P.failure(); + } + + const [, prefix, label,,, url] = result.value; + + const silent = (prefix === '?['); + if (typeof url === 'string') return P.failure(); + + return P.success(result.index, M.LINK(silent, url.props.url, mergeText(label))); }); }, - url: r => { + url: () => { const urlChar = P.regexp(/[.,a-z0-9_/:%#@$&?!~=+-]/i); - const innerItem: P.Parser = P.lazy(() => P.alt([ - P.seq([ + const innerItem: P.Parser = P.lazy(() => P.alt([ + P.seq( P.str('('), nest(innerItem, urlChar).many(0), P.str(')'), - ]), - P.seq([ + ), + P.seq( P.str('['), nest(innerItem, urlChar).many(0), P.str(']'), - ]), + ), urlChar, ])); - const parser = P.seq([ + const parser = P.seq( notLinkLabel, P.regexp(/https?:\/\//), innerItem.many(1).text(), - ]); + ); return new P.Parser((input, index, state) => { let result; result = parser.handler(input, index, state); @@ -700,16 +745,16 @@ export const language = P.createLanguage({ }); }, - urlAlt: r => { + urlAlt: () => { const open = P.str('<'); const close = P.str('>'); - const parser = P.seq([ + const parser = P.seq( notLinkLabel, open, P.regexp(/https?:\/\//), - P.seq([P.notMatch(P.alt([close, space])), P.char], 1).many(1), + P.seq(P.notMatch(P.alt([close, space])), P.char).select(1).many(1), close, - ]).text(); + ).text(); return new P.Parser((input, index, state) => { const result = parser.handler(input, index, state); if (!result.success) { @@ -720,30 +765,30 @@ export const language = P.createLanguage({ }); }, - search: r => { + search: () => { const button = P.alt([ P.regexp(/\[(検索|search)\]/i), P.regexp(/(検索|search)/i), ]); - return P.seq([ + return P.seq( newLine.option(), P.lineBegin, - P.seq([ + P.seq( P.notMatch(P.alt([ newLine, - P.seq([space, button, P.lineEnd]), + P.seq(space, button, P.lineEnd), ])), P.char, - ], 1).many(1), + ).select(1).many(1), space, button, P.lineEnd, newLine.option(), - ]).map(result => { + ).map(result => { const query = result[2].join(''); return M.SEARCH(query, `${query}${result[3]}${result[4]}`); }); }, - text: r => P.char, + text: () => P.char, }); diff --git a/src/internal/util.ts b/src/internal/util.ts index 3f0cffc..4c3935b 100644 --- a/src/internal/util.ts +++ b/src/internal/util.ts @@ -1,13 +1,15 @@ import { isMfmBlock, MfmInline, MfmNode, MfmText, TEXT } from '../node'; -export function mergeText(nodes: ((T extends MfmInline ? MfmInline : MfmNode) | string)[]): (T | MfmText)[] { +type ArrayRecursive = T | Array>; + +export function mergeText(nodes: ArrayRecursive<((T extends MfmInline ? MfmInline : MfmNode) | string)>[]): (T | MfmText)[] { const dest: (T | MfmText)[] = []; const storedChars: string[] = []; /** * Generate a text node from the stored chars, And push it. */ - function generateText() { + function generateText(): void { if (storedChars.length > 0) { dest.push(TEXT(storedChars.join(''))); storedChars.length = 0; @@ -136,7 +138,7 @@ export function stringifyTree(nodes: MfmNode[]): string { // block -> inline : Yes // block -> block : Yes - let pushLf: boolean = true; + let pushLf = true; if (isMfmBlock(node)) { if (state === stringifyState.none) { pushLf = false; @@ -159,7 +161,7 @@ export function stringifyTree(nodes: MfmNode[]): string { return dest.map(n => stringifyNode(n)).join(''); } -export function inspectOne(node: MfmNode, action: (node: MfmNode) => void) { +export function inspectOne(node: MfmNode, action: (node: MfmNode) => void): void { action(node); if (node.children != null) { for (const child of node.children) { diff --git a/test-d/index.ts b/test-d/index.ts index 388aca3..423b5a3 100644 --- a/test-d/index.ts +++ b/test-d/index.ts @@ -5,6 +5,7 @@ import { expectType } from 'tsd'; import { NodeType, MfmUrl } from '../src'; +import * as P from '../src/internal/core'; describe('#NodeType', () => { test('returns node that has sprcified type', () => { @@ -12,3 +13,18 @@ describe('#NodeType', () => { expectType(x); }); }); + +describe('parser internals', () => { + test('seq', () => { + const first = null as unknown as P.Parser<'first'>; + const second = null as unknown as P.Parser<'second'>; + const third = null as unknown as P.Parser<'third' | 'third-second'>; + expectType>(P.seq(first, second, third)); + }); + test('alt', () => { + const first = null as unknown as P.Parser<'first'>; + const second = null as unknown as P.Parser<'second'>; + const third = null as unknown as P.Parser<'third' | 'third-second'>; + expectType>(P.alt([first, second, third])); + }); +}); diff --git a/test/parser.ts b/test/parser.ts index 50ae700..852699c 100644 --- a/test/parser.ts +++ b/test/parser.ts @@ -1192,6 +1192,14 @@ hoge`; ]; assert.deepStrictEqual(mfm.parse(input), output); }); + + test('bad url in url part', () => { + const input = "[test](http://..)"; + const output = [ + TEXT("[test](http://..)") + ]; + assert.deepStrictEqual(mfm.parse(input), output); + }) }); describe('fn', () => { From 7dbd9f288965d0c8d28c4b6e498be1de1e5dc786 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sat, 15 Jun 2024 14:05:57 +0900 Subject: [PATCH 2/3] feat: parse in parseSimple (#146) --- src/internal/parser.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/internal/parser.ts b/src/internal/parser.ts index d64b122..26029b1 100644 --- a/src/internal/parser.ts +++ b/src/internal/parser.ts @@ -170,6 +170,7 @@ export const language = P.createLanguage({ return P.alt([ r.unicodeEmoji, // Regexp r.emojiCode, // ":" + r.plainTag, // "" // to NOT parse emojiCode inside `` r.text, ]); }, From 9cd9aaebe4db49b80aae72b03ea3fa2018f5face Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Sat, 15 Jun 2024 16:13:59 +0900 Subject: [PATCH 3/3] fix type error (#147) * feat: parse in parseSimple * fix: type error * chore: update api.md --- etc/mfm-js.api.md | 2 +- src/node.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/etc/mfm-js.api.md b/etc/mfm-js.api.md index 6792d39..6aef754 100644 --- a/etc/mfm-js.api.md +++ b/etc/mfm-js.api.md @@ -190,7 +190,7 @@ export type MfmSearch = { }; // @public (undocumented) -export type MfmSimpleNode = MfmUnicodeEmoji | MfmEmojiCode | MfmText; +export type MfmSimpleNode = MfmUnicodeEmoji | MfmEmojiCode | MfmText | MfmPlain; // @public (undocumented) export type MfmSmall = { diff --git a/src/node.ts b/src/node.ts index 2f00438..29d8353 100644 --- a/src/node.ts +++ b/src/node.ts @@ -1,6 +1,6 @@ export type MfmNode = MfmBlock | MfmInline; -export type MfmSimpleNode = MfmUnicodeEmoji | MfmEmojiCode | MfmText; +export type MfmSimpleNode = MfmUnicodeEmoji | MfmEmojiCode | MfmText | MfmPlain; export type MfmBlock = MfmQuote | MfmSearch | MfmCodeBlock | MfmMathBlock | MfmCenter;