Merge remote-tracking branch 'misskey/develop' into from-upstream

This commit is contained in:
dakkar 2024-06-29 09:23:12 +01:00
commit 42a8f0d44d
11 changed files with 281 additions and 174 deletions

View file

@ -43,14 +43,21 @@ module.exports = {
'prefer-arrow-callback': ['error'], 'prefer-arrow-callback': ['error'],
'no-throw-literal': ['error'], 'no-throw-literal': ['error'],
'no-param-reassign': ['warn'], 'no-param-reassign': ['warn'],
'no-constant-condition': ['warn'], 'no-constant-condition': ['warn', {
checkLoops: false,
}],
'no-empty-pattern': ['warn'], '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-inferrable-types': ['warn'],
'@typescript-eslint/no-non-null-assertion': ['warn'], '@typescript-eslint/no-non-null-assertion': ['warn'],
'@typescript-eslint/explicit-function-return-type': ['warn'], '@typescript-eslint/explicit-function-return-type': ['warn'],
'@typescript-eslint/no-misused-promises': ['error', { '@typescript-eslint/no-misused-promises': ['error', {
'checksVoidReturn': false, 'checksVoidReturn': false,
}], }],
'@typescript-eslint/no-unused-vars': ['error', {
"argsIgnorePattern": "^_",
}]
}, },
}; };

View file

@ -190,7 +190,7 @@ export type MfmSearch = {
}; };
// @public (undocumented) // @public (undocumented)
export type MfmSimpleNode = MfmUnicodeEmoji | MfmEmojiCode | MfmText; export type MfmSimpleNode = MfmUnicodeEmoji | MfmEmojiCode | MfmText | MfmPlain;
// @public (undocumented) // @public (undocumented)
export type MfmSmall = { export type MfmSmall = {

View file

@ -2,7 +2,7 @@ import { performance } from 'perf_hooks';
import inputLine, { InputCanceledError } from './misc/inputLine'; import inputLine, { InputCanceledError } from './misc/inputLine';
import { parse } from '..'; import { parse } from '..';
async function entryPoint() { async function entryPoint(): Promise<void> {
console.log('intaractive parser'); console.log('intaractive parser');
while (true) { while (true) {

View file

@ -2,7 +2,7 @@ import { performance } from 'perf_hooks';
import inputLine, { InputCanceledError } from './misc/inputLine'; import inputLine, { InputCanceledError } from './misc/inputLine';
import { parseSimple } from '..'; import { parseSimple } from '..';
async function entryPoint() { async function entryPoint(): Promise<void> {
console.log('intaractive simple parser'); console.log('intaractive simple parser');
while (true) { while (true) {

View file

@ -21,10 +21,17 @@ export type Failure = { success: false };
*/ */
export type Result<T> = Success<T> | Failure; export type Result<T> = Success<T> | Failure;
interface State {
trace?: boolean,
linkLabel?: boolean,
nestLimit: number,
depth: number,
}
/** /**
* The function used by a parser * The function used by a parser
*/ */
export type ParserHandler<T> = (input: string, index: number, state: any) => Result<T> export type ParserHandler<T> = (input: string, index: number, state: State) => Result<T>
/** /**
* A function that always returns a parse success. * A function that always returns a parse success.
@ -58,7 +65,7 @@ export class Parser<T> {
public handler: ParserHandler<T>; public handler: ParserHandler<T>;
constructor(handler: ParserHandler<T>, name?: string) { constructor(handler: ParserHandler<T>, name?: string) {
this.handler = (input, index, state) => { this.handler = (input, index, state) : Failure | Success<T> => {
if (state.trace && this.name != null) { if (state.trace && this.name != null) {
const pos = `${index}`; const pos = `${index}`;
console.log(`${pos.padEnd(6, ' ')}enter ${this.name}`); console.log(`${pos.padEnd(6, ' ')}enter ${this.name}`);
@ -145,17 +152,21 @@ export class Parser<T> {
* @param min The minimum amount of times the separator must appear. * @param min The minimum amount of times the separator must appear.
* @returns A {@link Success} object if the minimum separator count is met, and a {@link Failure} otherwise. * @returns A {@link Success} object if the minimum separator count is met, and a {@link Failure} otherwise.
*/ */
sep(separator: Parser<any>, min: number): Parser<T[]> { sep(separator: Parser<unknown>, min: number): Parser<T[]> {
if (min < 1) { if (min < 1) {
throw new Error('"min" must be a value greater than or equal to 1.'); throw new Error('"min" must be a value greater than or equal to 1.');
} }
return seq([ return seq(
this, this,
seq([ seq(
separator, separator,
this, this,
], 1).many(min - 1), ).select(1).many(min - 1),
]).map(result => [result[0], ...result[1]]); ).map(result => [result[0], ...result[1]]);
}
select<K extends keyof T>(key: K): Parser<T[K]> {
return this.map(v => v[key]);
} }
/** /**
@ -164,7 +175,7 @@ export class Parser<T> {
* *
* @returns A {@link Success} object. * @returns A {@link Success} object.
*/ */
option<T>(): Parser<T | null> { option(): Parser<T | null> {
return alt([ return alt([
this, this,
succeeded(null), succeeded(null),
@ -208,6 +219,16 @@ export function regexp<T extends RegExp>(pattern: T): Parser<string> {
}); });
} }
type ParsedType<T extends Parser<unknown>> = T extends Parser<infer U> ? U : never;
export type SeqParseResult<T extends unknown[]> =
T extends [] ? []
: T extends [infer F, ...infer R]
? (
F extends Parser<unknown> ? [ParsedType<F>, ...SeqParseResult<R>] : [unknown, ...SeqParseResult<R>]
)
: unknown[];
/** /**
* A function that returns a {@link Parser} that goes through the parsers provided, in order, and checks that they all * A function that returns a {@link Parser} that goes through the parsers provided, in order, and checks that they all
* succeed. A {@link Failure} object is returned if any of the parsers fails. * succeed. A {@link Failure} object is returned if any of the parsers fails.
@ -222,7 +243,7 @@ export function regexp<T extends RegExp>(pattern: T): Parser<string> {
* a value based on the state of `select` (the entire array if `null`, else the value held at the * a value based on the state of `select` (the entire array if `null`, else the value held at the
* index specified by `select`). * index specified by `select`).
*/ */
export function seq(parsers: Parser<any>[], select?: number): Parser<any> { export function seq<Parsers extends Parser<unknown>[]>(...parsers: Parsers): Parser<SeqParseResult<Parsers>> {
return new Parser((input, index, state) => { return new Parser((input, index, state) => {
let result; let result;
let latestIndex = index; let latestIndex = index;
@ -235,7 +256,7 @@ export function seq(parsers: Parser<any>[], select?: number): Parser<any> {
latestIndex = result.index; latestIndex = result.index;
accum.push(result.value); accum.push(result.value);
} }
return success(latestIndex, (select != null ? accum[select] : accum)); return success(latestIndex, accum as SeqParseResult<Parsers>);
}); });
} }
@ -246,13 +267,13 @@ export function seq(parsers: Parser<any>[], select?: number): Parser<any> {
* @param parsers The {@link Parser Parsers} that should be used. * @param parsers The {@link Parser Parsers} that should be used.
* @returns A {@link Parser} that returns the first {@link Success} from the supplied parsers. * @returns A {@link Parser} that returns the first {@link Success} from the supplied parsers.
*/ */
export function alt(parsers: Parser<any>[]): Parser<any> { export function alt<Parsers extends Parser<unknown>[]>(parsers: Parsers): Parser<ParsedType<Parsers[number]>> {
return new Parser((input, index, state) => { return new Parser<ParsedType<Parsers[number]>>((input, index, state): Result<ParsedType<Parsers[number]>> => {
let result;
for (let i = 0; i < parsers.length; i++) { 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) { if (result.success) {
return result; return result as Result<ParsedType<Parsers[number]>>;
} }
} }
return failure(); return failure();
@ -277,7 +298,7 @@ function succeeded<T>(value: T): Parser<T> {
* @param parser The {@link Parser} to be matched. * @param parser The {@link Parser} to be matched.
* @returns A {@link Success} with the value `null` if the parser fails, or a {@link Failure} if it succeeds. * @returns A {@link Success} with the value `null` if the parser fails, or a {@link Failure} if it succeeds.
*/ */
export function notMatch(parser: Parser<any>): Parser<null> { export function notMatch(parser: Parser<unknown>): Parser<null> {
return new Parser((input, index, state) => { return new Parser((input, index, state) => {
const result = parser.handler(input, index, state); const result = parser.handler(input, index, state);
return !result.success return !result.success
@ -380,12 +401,15 @@ export function lazy<T>(fn: () => Parser<T>): Parser<T> {
//type SyntaxReturn<T> = T extends (rules: Record<string, Parser<any>>) => infer R ? R : never; //type SyntaxReturn<T> = T extends (rules: Record<string, Parser<any>>) => infer R ? R : never;
//export function createLanguage2<T extends Record<string, Syntax<any>>>(syntaxes: T): { [K in keyof T]: SyntaxReturn<T[K]> } { //export function createLanguage2<T extends Record<string, Syntax<any>>>(syntaxes: T): { [K in keyof T]: SyntaxReturn<T[K]> } {
type ParserTable<T> = { [K in keyof T]: Parser<T[K]> };
// TODO: 関数の型宣言をいい感じにしたい // TODO: 関数の型宣言をいい感じにしたい
export function createLanguage<T>(syntaxes: { [K in keyof T]: (r: Record<string, Parser<any>>) => T[K] }): T { export function createLanguage<T>(syntaxes: { [K in keyof T]: (r: ParserTable<T>) => Parser<T[K]> }): ParserTable<T> {
const rules: Record<string, Parser<any>> = {}; // @ts-expect-error initializing object so type error here
for (const key of Object.keys(syntaxes)) { const rules: ParserTable<T> = {};
for (const key of Object.keys(syntaxes) as (keyof T & string)[]) {
rules[key] = lazy(() => { rules[key] = lazy(() => {
const parser = (syntaxes as any)[key](rules); const parser = syntaxes[key](rules);
if (parser == null) { if (parser == null) {
throw new Error('syntax must return a parser.'); throw new Error('syntax must return a parser.');
} }
@ -393,5 +417,5 @@ export function createLanguage<T>(syntaxes: { [K in keyof T]: (r: Record<string,
return parser; return parser;
}); });
} }
return rules as any; return rules;
} }

View file

@ -1,7 +1,6 @@
import * as M from '..'; import * as M from '..';
import { language } from './parser'; import { language } from './parser';
import { mergeText } from './util'; import { mergeText } from './util';
import * as P from './core';
/** /**
* A type representing the available options for the full parser. * A type representing the available options for the full parser.
@ -24,7 +23,8 @@ export function fullParser(input: string, opts: FullParserOpts): M.MfmNode[] {
depth: 0, depth: 0,
linkLabel: false, linkLabel: false,
trace: false, trace: false,
}) as P.Success<any>; });
if (!result.success) throw new Error('Unexpected parse error');
return mergeText(result.value); return mergeText(result.value);
} }
@ -36,6 +36,10 @@ export function fullParser(input: string, opts: FullParserOpts): M.MfmNode[] {
* @returns An array of simple nodes represennting the resulting styles. * @returns An array of simple nodes represennting the resulting styles.
*/ */
export function simpleParser(input: string): M.MfmSimpleNode[] { export function simpleParser(input: string): M.MfmSimpleNode[] {
const result = language.simpleParser.handler(input, 0, { }) as P.Success<any>; 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); return mergeText(result.value);
} }

View file

@ -1,6 +1,7 @@
import * as M from '..'; import * as M from '..';
import * as P from './core'; import * as P from './core';
import { mergeText } from './util'; import { mergeText } from './util';
import { SeqParseResult } from './core';
// NOTE: // NOTE:
// tsdのテストでファイルを追加しているにも関わらず「@twemoji/parser/dist/lib/regex」の型定義ファイルがないとエラーが出るため、 // tsdのテストでファイルを追加しているにも関わらず「@twemoji/parser/dist/lib/regex」の型定義ファイルがないとエラーが出るため、
@ -31,9 +32,10 @@ const newLine = P.alt([P.crlf, P.cr, P.lf]);
* fails, the matched portion of the input text is returned. If all succeed, the array of each * fails, the matched portion of the input text is returned. If all succeed, the array of each
* {@link P.Success Success'} value is returned. * {@link P.Success Success'} value is returned.
*/ */
function seqOrText(parsers: P.Parser<any>[]): P.Parser<any[] | string> { function seqOrText<Parsers extends P.Parser<unknown>[]>(...parsers: Parsers): P.Parser<SeqParseResult<Parsers> | string> {
return new P.Parser<any[] | string>((input, index, state) => { return new P.Parser<SeqParseResult<Parsers> | string>((input, index, state) => {
const accum: any[] = []; // TODO: typesafe implementation
const accum: unknown[] = [];
let latestIndex = index; let latestIndex = index;
for (let i = 0 ; i < parsers.length; i++) { for (let i = 0 ; i < parsers.length; i++) {
const result = parsers[i].handler(input, latestIndex, state); const result = parsers[i].handler(input, latestIndex, state);
@ -47,7 +49,7 @@ function seqOrText(parsers: P.Parser<any>[]): P.Parser<any[] | string> {
accum.push(result.value); accum.push(result.value);
latestIndex = result.index; latestIndex = result.index;
} }
return P.success(latestIndex, accum); return P.success(latestIndex, accum as SeqParseResult<Parsers>);
}); });
} }
@ -82,7 +84,7 @@ const nestable = new P.Parser((_input, index, state) => {
function nest<T>(parser: P.Parser<T>, fallback?: P.Parser<string>): P.Parser<T | string> { function nest<T>(parser: P.Parser<T>, fallback?: P.Parser<string>): P.Parser<T | string> {
// nesting limited? -> No: specified parser, Yes: fallback parser (default = P.char) // nesting limited? -> No: specified parser, Yes: fallback parser (default = P.char)
const inner = P.alt([ const inner = P.alt([
P.seq([nestable, parser], 1), P.seq(nestable, parser).select(1),
(fallback != null) ? fallback : P.char, (fallback != null) ? fallback : P.char,
]); ]);
return new P.Parser<T | string>((input, index, state) => { return new P.Parser<T | string>((input, index, state) => {
@ -93,10 +95,45 @@ function nest<T>(parser: P.Parser<T>, fallback?: P.Parser<string>): P.Parser<T |
}); });
} }
interface TypeTable {
fullParser: (M.MfmNode | string)[],
simpleParser: (M.MfmSimpleNode | string)[],
full: M.MfmNode | string,
simple: M.MfmSimpleNode | string,
inline: M.MfmInline | string,
quote: M.NodeType<'quote'>,
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,
}
/** /**
* The collection of {@link P.Parser Parsers} representing the rules that make up SFM * The collection of {@link P.Parser Parsers} representing the rules that make up SFM
*/ */
export const language = P.createLanguage({ export const language = P.createLanguage<TypeTable>({
/** /**
* A {@link P.Parser Parser} that matches every SFM rule as many times as possible. * A {@link P.Parser Parser} that matches every SFM rule as many times as possible.
* *
@ -193,6 +230,7 @@ export const language = P.createLanguage({
return P.alt([ return P.alt([
r.unicodeEmoji, // Regexp r.unicodeEmoji, // Regexp
r.emojiCode, // ":" r.emojiCode, // ":"
r.plainTag, // "<plain>" // to NOT parse emojiCode inside `<plain>`
r.text, r.text,
]); ]);
}, },
@ -258,19 +296,19 @@ export const language = P.createLanguage({
* @returns The {@link P.Parser Parser} for this rule * @returns The {@link P.Parser Parser} for this rule
*/ */
quote: r => { quote: r => {
const lines: P.Parser<string[]> = P.seq([ const lines: P.Parser<string[]> = P.seq(
P.str('>'), P.str('>'),
space.option(), space.option(),
P.seq([P.notMatch(newLine), P.char], 1).many(0).text(), P.seq(P.notMatch(newLine), P.char).select(1).many(0).text(),
], 2).sep(newLine, 1); ).select(2).sep(newLine, 1);
const parser = P.seq([ const parser = P.seq(
newLine.option(), newLine.option(),
newLine.option(), newLine.option(),
P.lineBegin, P.lineBegin,
lines, lines,
newLine.option(), newLine.option(),
newLine.option(), newLine.option(),
], 3); ).select(3);
return new P.Parser((input, index, state) => { return new P.Parser((input, index, state) => {
let result; let result;
// parse quote // parse quote
@ -302,22 +340,22 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns The {@link P.Parser Parser} for this rule * @returns The {@link P.Parser Parser} for this rule
*/ */
codeBlock: r => { codeBlock: () => {
const mark = P.str('```'); const mark = P.str('```');
return P.seq([ return P.seq(
newLine.option(), newLine.option(),
P.lineBegin, P.lineBegin,
mark, mark,
P.seq([P.notMatch(newLine), P.char], 1).many(0), P.seq(P.notMatch(newLine), P.char).select(1).many(0),
newLine, 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, newLine,
mark, mark,
P.lineEnd, P.lineEnd,
newLine.option(), newLine.option(),
]).map(result => { ).map(result => {
const lang = (result[3] as string[]).join('').trim(); const lang = result[3].join('').trim();
const code = (result[5] as string[]).join(''); const code = result[5].join('');
return M.CODE_BLOCK(code, (lang.length > 0 ? lang : null)); return M.CODE_BLOCK(code, (lang.length > 0 ? lang : null));
}); });
}, },
@ -329,19 +367,19 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns The {@link P.Parser Parser} for this rule * @returns The {@link P.Parser Parser} for this rule
*/ */
mathBlock: r => { mathBlock: () => {
const open = P.str('\\['); const open = P.str('\\[');
const close = P.str('\\]'); const close = P.str('\\]');
return P.seq([ return P.seq(
newLine.option(), newLine.option(),
open, open,
newLine.option(), 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(), newLine.option(),
close, close,
newLine.option(), newLine.option(),
]).map(result => { ).map(result => {
const formula = (result[3] as string[]).join(''); const formula = result[3].join('');
return M.MATH_BLOCK(formula); return M.MATH_BLOCK(formula);
}); });
}, },
@ -357,17 +395,17 @@ export const language = P.createLanguage({
centerTag: r => { centerTag: r => {
const open = P.str('<center>'); const open = P.str('<center>');
const close = P.str('</center>'); const close = P.str('</center>');
return P.seq([ return P.seq(
newLine.option(), newLine.option(),
P.lineBegin, P.lineBegin,
open, open,
newLine.option(), 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(), newLine.option(),
close, close,
P.lineEnd, P.lineEnd,
newLine.option(), newLine.option(),
]).map(result => { ).map(result => {
return M.CENTER(mergeText(result[4])); return M.CENTER(mergeText(result[4]));
}); });
}, },
@ -381,11 +419,11 @@ export const language = P.createLanguage({
*/ */
big: r => { big: r => {
const mark = P.str('***'); const mark = P.str('***');
return seqOrText([ return seqOrText(
mark, 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, mark,
]).map(result => { ).map(result => {
if (typeof result === 'string') return result; if (typeof result === 'string') return result;
return M.FN('tada', {}, mergeText(result[1])); return M.FN('tada', {}, mergeText(result[1]));
}); });
@ -400,13 +438,13 @@ export const language = P.createLanguage({
*/ */
boldAsta: r => { boldAsta: r => {
const mark = P.str('**'); const mark = P.str('**');
return seqOrText([ return seqOrText(
mark, 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, mark,
]).map(result => { ).map(result => {
if (typeof result === 'string') return result; if (typeof result === 'string') return result;
return M.BOLD(mergeText(result[1] as (M.MfmInline | string)[])); return M.BOLD(mergeText(result[1]));
}); });
}, },
@ -420,13 +458,13 @@ export const language = P.createLanguage({
boldTag: r => { boldTag: r => {
const open = P.str('<b>'); const open = P.str('<b>');
const close = P.str('</b>'); const close = P.str('</b>');
return seqOrText([ return seqOrText(
open, 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, close,
]).map(result => { ).map(result => {
if (typeof result === 'string') return result; if (typeof result === 'string') return result;
return M.BOLD(mergeText(result[1] as (M.MfmInline | string)[])); return M.BOLD(mergeText(result[1]));
}); });
}, },
@ -437,13 +475,13 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns The {@link P.Parser Parser} for this rule * @returns The {@link P.Parser Parser} for this rule
*/ */
boldUnder: r => { boldUnder: () => {
const mark = P.str('__'); const mark = P.str('__');
return P.seq([ return P.seq(
mark, mark,
P.alt([alphaAndNum, space]).many(1), P.alt([alphaAndNum, space]).many(1),
mark, mark,
]).map(result => M.BOLD(mergeText(result[1] as string[]))); ).map(result => M.BOLD(mergeText(result[1])));
}, },
/** /**
@ -456,13 +494,13 @@ export const language = P.createLanguage({
smallTag: r => { smallTag: r => {
const open = P.str('<small>'); const open = P.str('<small>');
const close = P.str('</small>'); const close = P.str('</small>');
return seqOrText([ return seqOrText(
open, 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, close,
]).map(result => { ).map(result => {
if (typeof result === 'string') return result; if (typeof result === 'string') return result;
return M.SMALL(mergeText(result[1] as (M.MfmInline | string)[])); return M.SMALL(mergeText(result[1]));
}); });
}, },
@ -476,13 +514,13 @@ export const language = P.createLanguage({
italicTag: r => { italicTag: r => {
const open = P.str('<i>'); const open = P.str('<i>');
const close = P.str('</i>'); const close = P.str('</i>');
return seqOrText([ return seqOrText(
open, 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, close,
]).map(result => { ).map(result => {
if (typeof result === 'string') return result; if (typeof result === 'string') return result;
return M.ITALIC(mergeText(result[1] as (M.MfmInline | string)[])); return M.ITALIC(mergeText(result[1]));
}); });
}, },
@ -493,13 +531,13 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns The {@link P.Parser Parser} for this rule * @returns The {@link P.Parser Parser} for this rule
*/ */
italicAsta: r => { italicAsta: () => {
const mark = P.str('*'); const mark = P.str('*');
const parser = P.seq([ const parser = P.seq(
mark, mark,
P.alt([alphaAndNum, space]).many(1), P.alt([alphaAndNum, space]).many(1),
mark, mark,
]); );
return new P.Parser((input, index, state) => { return new P.Parser((input, index, state) => {
const result = parser.handler(input, index, state); const result = parser.handler(input, index, state);
if (!result.success) { if (!result.success) {
@ -510,7 +548,7 @@ export const language = P.createLanguage({
if (/[a-z0-9]$/i.test(beforeStr)) { if (/[a-z0-9]$/i.test(beforeStr)) {
return P.failure(); 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])));
}); });
}, },
@ -521,13 +559,13 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns The {@link P.Parser Parser} for this rule * @returns The {@link P.Parser Parser} for this rule
*/ */
italicUnder: r => { italicUnder: () => {
const mark = P.str('_'); const mark = P.str('_');
const parser = P.seq([ const parser = P.seq(
mark, mark,
P.alt([alphaAndNum, space]).many(1), P.alt([alphaAndNum, space]).many(1),
mark, mark,
]); );
return new P.Parser((input, index, state) => { return new P.Parser((input, index, state) => {
const result = parser.handler(input, index, state); const result = parser.handler(input, index, state);
if (!result.success) { if (!result.success) {
@ -538,7 +576,7 @@ export const language = P.createLanguage({
if (/[a-z0-9]$/i.test(beforeStr)) { if (/[a-z0-9]$/i.test(beforeStr)) {
return P.failure(); 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])));
}); });
}, },
@ -552,13 +590,13 @@ export const language = P.createLanguage({
strikeTag: r => { strikeTag: r => {
const open = P.str('<s>'); const open = P.str('<s>');
const close = P.str('</s>'); const close = P.str('</s>');
return seqOrText([ return seqOrText(
open, 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, close,
]).map(result => { ).map(result => {
if (typeof result === 'string') return result; if (typeof result === 'string') return result;
return M.STRIKE(mergeText(result[1] as (M.MfmInline | string)[])); return M.STRIKE(mergeText(result[1]));
}); });
}, },
@ -571,13 +609,13 @@ export const language = P.createLanguage({
*/ */
strikeWave: r => { strikeWave: r => {
const mark = P.str('~~'); const mark = P.str('~~');
return seqOrText([ return seqOrText(
mark, 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, mark,
]).map(result => { ).map(result => {
if (typeof result === 'string') return result; if (typeof result === 'string') return result;
return M.STRIKE(mergeText(result[1] as (M.MfmInline | string)[])); return M.STRIKE(mergeText(result[1]));
}); });
}, },
@ -587,7 +625,7 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns The {@link P.Parser Parser} for this rule * @returns The {@link P.Parser Parser} for this rule
*/ */
unicodeEmoji: r => { unicodeEmoji: () => {
const emoji = RegExp(twemojiRegex.source); const emoji = RegExp(twemojiRegex.source);
return P.regexp(emoji).map(content => M.UNI_EMOJI(content)); return P.regexp(emoji).map(content => M.UNI_EMOJI(content));
}, },
@ -599,19 +637,19 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns The {@link P.Parser Parser} for this rule * @returns The {@link P.Parser Parser} for this rule
*/ */
plainTag: r => { plainTag: () => {
const open = P.str('<plain>'); const open = P.str('<plain>');
const close = P.str('</plain>'); const close = P.str('</plain>');
return P.seq([ return P.seq(
open, open,
newLine.option(), newLine.option(),
P.seq([ P.seq(
P.notMatch(P.seq([newLine.option(), close])), P.notMatch(P.seq(newLine.option(), close)),
P.char, P.char,
], 1).many(1).text(), ).select(1).many(1).text(),
newLine.option(), newLine.option(),
close, close,
], 2).map(result => M.PLAIN(result)); ).select(2).map(result => M.PLAIN(result));
}, },
fn: r => { fn: r => {
@ -622,22 +660,22 @@ export const language = P.createLanguage({
} }
return P.success(result.index, result.value); return P.success(result.index, result.value);
}); });
const arg: P.Parser<ArgPair> = P.seq([ const arg: P.Parser<ArgPair> = P.seq(
P.regexp(/[a-z0-9_]+/i), P.regexp(/[a-z0-9_]+/i),
P.seq([ P.seq(
P.str('='), P.str('='),
P.regexp(/[a-z0-9_.-]+/i), P.regexp(/[a-z0-9_.-]+/i),
], 1).option(), ).select(1).option(),
]).map(result => { ).map(result => {
return { return {
k: result[0], k: result[0],
v: (result[1] != null) ? result[1] : true, v: (result[1] != null) ? result[1] : true,
}; };
}); });
const args = P.seq([ const args = P.seq(
P.str('.'), P.str('.'),
arg.sep(P.str(','), 1), arg.sep(P.str(','), 1),
], 1).map(pairs => { ).select(1).map(pairs => {
const result: Args = { }; const result: Args = { };
for (const pair of pairs) { for (const pair of pairs) {
result[pair.k] = pair.v; result[pair.k] = pair.v;
@ -645,17 +683,17 @@ export const language = P.createLanguage({
return result; return result;
}); });
const fnClose = P.str(']'); const fnClose = P.str(']');
return seqOrText([ return seqOrText(
P.str('$['), P.str('$['),
fnName, fnName,
args.option(), args.option(),
P.str(' '), 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, fnClose,
]).map(result => { ).map(result => {
if (typeof result === 'string') return result; if (typeof result === 'string') return result;
const name = result[1]; const name = result[1];
const args = result[2] || {}; const args: Args = result[2] || {};
const content = result[4]; const content = result[4];
return M.FN(name, args, mergeText(content)); return M.FN(name, args, mergeText(content));
}); });
@ -668,16 +706,16 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns The {@link P.Parser Parser} for this rule * @returns The {@link P.Parser Parser} for this rule
*/ */
inlineCode: r => { inlineCode: () => {
const mark = P.str('`'); const mark = P.str('`');
return P.seq([ return P.seq(
mark, mark,
P.seq([ P.seq(
P.notMatch(P.alt([mark, P.str('´'), newLine])), P.notMatch(P.alt([mark, P.str('´'), newLine])),
P.char, P.char,
], 1).many(1), ).select(1).many(1),
mark, mark,
]).map(result => M.INLINE_CODE(result[1].join(''))); ).map(result => M.INLINE_CODE(result[1].join('')));
}, },
/** /**
@ -687,17 +725,17 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns The {@link P.Parser Parser} for this rule * @returns The {@link P.Parser Parser} for this rule
*/ */
mathInline: r => { mathInline: () => {
const open = P.str('\\('); const open = P.str('\\(');
const close = P.str('\\)'); const close = P.str('\\)');
return P.seq([ return P.seq(
open, open,
P.seq([ P.seq(
P.notMatch(P.alt([close, newLine])), P.notMatch(P.alt([close, newLine])),
P.char, P.char,
], 1).many(1), ).select(1).many(1),
close, close,
]).map(result => M.MATH_INLINE(result[1].join(''))); ).map(result => M.MATH_INLINE(result[1].join('')));
}, },
/** /**
@ -708,16 +746,16 @@ export const language = P.createLanguage({
* @param r the rules of SFM * @param r the rules of SFM
* @returns The {@link P.Parser Parser} for this rule * @returns The {@link P.Parser Parser} for this rule
*/ */
mention: r => { mention: () => {
const parser = P.seq([ const parser = P.seq(
notLinkLabel, notLinkLabel,
P.str('@'), P.str('@'),
P.regexp(/[a-z0-9_.-]+/i), P.regexp(/[a-z0-9_.-]+/i),
P.seq([ P.seq(
P.str('@'), P.str('@'),
P.regexp(/[a-z0-9_.-]+/i), P.regexp(/[a-z0-9_.-]+/i),
], 1).option(), ).select(1).option(),
]); );
return new P.Parser<M.MfmMention | string>((input, index, state) => { return new P.Parser<M.MfmMention | string>((input, index, state) => {
let result; let result;
result = parser.handler(input, index, state); result = parser.handler(input, index, state);
@ -783,32 +821,32 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns A {@link P.Parser Parser} for this rule * @returns A {@link P.Parser Parser} for this rule
*/ */
hashtag: r => { hashtag: () => {
const mark = P.str('#'); const mark = P.str('#');
const hashTagChar = P.seq([ const hashTagChar = P.seq(
P.notMatch(P.regexp(/[\s.,\u2063!?'"#:/[\]()<>]/u)), P.notMatch(P.regexp(/[\s.,\u2063!?'"#:/[\]()<>]/u)),
P.char, P.char,
], 1); ).select(1);
const innerItem: P.Parser<any> = P.lazy(() => P.alt([ const innerItem: P.Parser<unknown> = P.lazy(() => P.alt([
P.seq([ P.seq(
P.str('('), nest(innerItem, hashTagChar).many(0), P.str(')'), P.str('('), nest(innerItem, hashTagChar).many(0), P.str(')'),
]), ),
P.seq([ P.seq(
P.str('['), nest(innerItem, hashTagChar).many(0), P.str(']'), P.str('['), nest(innerItem, hashTagChar).many(0), P.str(']'),
]), ),
P.seq([ P.seq(
P.str('「'), nest(innerItem, hashTagChar).many(0), P.str('」'), P.str('「'), nest(innerItem, hashTagChar).many(0), P.str('」'),
]), ),
P.seq([ P.seq(
P.str(''), nest(innerItem, hashTagChar).many(0), P.str(''), P.str(''), nest(innerItem, hashTagChar).many(0), P.str(''),
]), ),
hashTagChar, hashTagChar,
])); ]));
const parser = P.seq([ const parser = P.seq(
notLinkLabel, notLinkLabel,
mark, mark,
innerItem.many(1).text(), innerItem.many(1).text(),
], 2); ).select(2);
return new P.Parser((input, index, state) => { return new P.Parser((input, index, state) => {
const result = parser.handler(input, index, state); const result = parser.handler(input, index, state);
if (!result.success) { if (!result.success) {
@ -837,16 +875,16 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns A {@link P.Parser Parser} for this rule * @returns A {@link P.Parser Parser} for this rule
*/ */
emojiCode: r => { emojiCode: () => {
const side = P.notMatch(P.regexp(/[a-z0-9]/i)); const side = P.notMatch(P.regexp(/[a-z0-9]/i));
const mark = P.str(':'); const mark = P.str(':');
return P.seq([ return P.seq(
P.alt([P.lineBegin, side]), P.alt([P.lineBegin, side]),
mark, mark,
P.regexp(/[\p{Letter}\p{Number}\p{Mark}_+-]+/iu), P.regexp(/[\p{Letter}\p{Number}\p{Mark}_+-]+/iu),
mark, mark,
P.alt([P.lineEnd, side]), P.alt([P.lineEnd, side]),
], 2).map(name => M.EMOJI_CODE((name as string).normalize('NFC'))); ).select(2).map(name => M.EMOJI_CODE(name.normalize('NFC')));
}, },
/** /**
@ -867,22 +905,30 @@ export const language = P.createLanguage({
return result; return result;
}); });
const closeLabel = P.str(']'); const closeLabel = P.str(']');
return P.seq([ const parser = P.seq(
notLinkLabel, notLinkLabel,
P.alt([P.str('?['), P.str('[')]), P.alt([P.str('?['), P.str('[')]),
P.seq([ P.seq(
P.notMatch(P.alt([closeLabel, newLine])), P.notMatch(P.alt([closeLabel, newLine])),
nest(labelInline), nest(labelInline),
], 1).many(1), ).select(1).many(1),
closeLabel, closeLabel,
P.str('('), P.str('('),
P.alt([r.urlAlt, r.url]), P.alt([r.urlAlt, r.url]),
P.str(')'), P.str(')'),
]).map(result => { );
const silent = (result[1] === '?['); return new P.Parser<M.MfmLink>((input, index, state) => {
const label = result[2]; const result = parser.handler(input, index, state);
const url: M.MfmUrl = result[5]; if (!result.success) {
return M.LINK(silent, url.props.url, mergeText(label)); 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)));
}); });
}, },
@ -892,22 +938,22 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns A {@link P.Parser Parser} for this rule * @returns A {@link P.Parser Parser} for this rule
*/ */
url: r => { url: () => {
const urlChar = P.regexp(/[.,a-z0-9_/:%#@$&?!~=+-]/i); const urlChar = P.regexp(/[.,a-z0-9_/:%#@$&?!~=+-]/i);
const innerItem: P.Parser<any> = P.lazy(() => P.alt([ const innerItem: P.Parser<unknown> = P.lazy(() => P.alt([
P.seq([ P.seq(
P.str('('), nest(innerItem, urlChar).many(0), P.str(')'), P.str('('), nest(innerItem, urlChar).many(0), P.str(')'),
]), ),
P.seq([ P.seq(
P.str('['), nest(innerItem, urlChar).many(0), P.str(']'), P.str('['), nest(innerItem, urlChar).many(0), P.str(']'),
]), ),
urlChar, urlChar,
])); ]));
const parser = P.seq([ const parser = P.seq(
notLinkLabel, notLinkLabel,
P.regexp(/https?:\/\//), P.regexp(/https?:\/\//),
innerItem.many(1).text(), innerItem.many(1).text(),
]); );
return new P.Parser<M.MfmUrl | string>((input, index, state) => { return new P.Parser<M.MfmUrl | string>((input, index, state) => {
let result; let result;
result = parser.handler(input, index, state); result = parser.handler(input, index, state);
@ -937,16 +983,16 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns A {@link P.Parser Parser} for this rule * @returns A {@link P.Parser Parser} for this rule
*/ */
urlAlt: r => { urlAlt: () => {
const open = P.str('<'); const open = P.str('<');
const close = P.str('>'); const close = P.str('>');
const parser = P.seq([ const parser = P.seq(
notLinkLabel, notLinkLabel,
open, open,
P.regexp(/https?:\/\//), 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, close,
]).text(); ).text();
return new P.Parser((input, index, state) => { return new P.Parser((input, index, state) => {
const result = parser.handler(input, index, state); const result = parser.handler(input, index, state);
if (!result.success) { if (!result.success) {
@ -965,25 +1011,25 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns A {@link P.Parser Parser} for this rule * @returns A {@link P.Parser Parser} for this rule
*/ */
search: r => { search: () => {
const button = P.alt([ const button = P.alt([
P.regexp(/\[(検索|search)\]/i), P.regexp(/\[(検索|search)\]/i),
]); ]);
return P.seq([ return P.seq(
newLine.option(), newLine.option(),
P.lineBegin, P.lineBegin,
P.seq([ P.seq(
P.notMatch(P.alt([ P.notMatch(P.alt([
newLine, newLine,
P.seq([space, button, P.lineEnd]), P.seq(space, button, P.lineEnd),
])), ])),
P.char, P.char,
], 1).many(1), ).select(1).many(1),
space, space,
button, button,
P.lineEnd, P.lineEnd,
newLine.option(), newLine.option(),
]).map(result => { ).map(result => {
const query = result[2].join(''); const query = result[2].join('');
return M.SEARCH(query, `${query}${result[3]}${result[4]}`); return M.SEARCH(query, `${query}${result[3]}${result[4]}`);
}); });
@ -995,5 +1041,5 @@ export const language = P.createLanguage({
* @param r The rules of SFM * @param r The rules of SFM
* @returns A {@link P.Parser Parser} for this rule * @returns A {@link P.Parser Parser} for this rule
*/ */
text: r => P.char, text: () => P.char,
}); });

View file

@ -1,13 +1,15 @@
import { isMfmBlock, MfmInline, MfmNode, MfmText, TEXT } from '../node'; import { isMfmBlock, MfmInline, MfmNode, MfmText, TEXT } from '../node';
export function mergeText<T extends MfmNode>(nodes: ((T extends MfmInline ? MfmInline : MfmNode) | string)[]): (T | MfmText)[] { type ArrayRecursive<T> = T | Array<ArrayRecursive<T>>;
export function mergeText<T extends MfmNode>(nodes: ArrayRecursive<((T extends MfmInline ? MfmInline : MfmNode) | string)>[]): (T | MfmText)[] {
const dest: (T | MfmText)[] = []; const dest: (T | MfmText)[] = [];
const storedChars: string[] = []; const storedChars: string[] = [];
/** /**
* Generate a text node from the stored chars, And push it. * Generate a text node from the stored chars, And push it.
*/ */
function generateText() { function generateText(): void {
if (storedChars.length > 0) { if (storedChars.length > 0) {
dest.push(TEXT(storedChars.join(''))); dest.push(TEXT(storedChars.join('')));
storedChars.length = 0; storedChars.length = 0;
@ -136,7 +138,7 @@ export function stringifyTree(nodes: MfmNode[]): string {
// block -> inline : Yes // block -> inline : Yes
// block -> block : Yes // block -> block : Yes
let pushLf: boolean = true; let pushLf = true;
if (isMfmBlock(node)) { if (isMfmBlock(node)) {
if (state === stringifyState.none) { if (state === stringifyState.none) {
pushLf = false; pushLf = false;
@ -159,7 +161,7 @@ export function stringifyTree(nodes: MfmNode[]): string {
return dest.map(n => stringifyNode(n)).join(''); 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); action(node);
if (node.children != null) { if (node.children != null) {
for (const child of node.children) { for (const child of node.children) {

View file

@ -1,6 +1,6 @@
export type MfmNode = MfmBlock | MfmInline; 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; export type MfmBlock = MfmQuote | MfmSearch | MfmCodeBlock | MfmMathBlock | MfmCenter;

View file

@ -5,6 +5,7 @@
import { expectType } from 'tsd'; import { expectType } from 'tsd';
import { NodeType, MfmUrl } from '../src'; import { NodeType, MfmUrl } from '../src';
import * as P from '../src/internal/core';
describe('#NodeType', () => { describe('#NodeType', () => {
test('returns node that has sprcified type', () => { test('returns node that has sprcified type', () => {
@ -12,3 +13,18 @@ describe('#NodeType', () => {
expectType<MfmUrl>(x); expectType<MfmUrl>(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.Parser<['first', 'second', 'third' | 'third-second']>>(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.Parser<'first' | 'second' | 'third' | 'third-second'>>(P.alt([first, second, third]));
});
});

View file

@ -1261,6 +1261,14 @@ hoge`;
]; ];
assert.deepStrictEqual(mfm.parse(input), output); 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', () => { describe('fn', () => {