mirror of
https://activitypub.software/TransFem-org/sfm-js
synced 2024-11-22 14:05:13 +00:00
Merge branch 'from-upstream' into 'develop'
upstream changes 2024-06-29 See merge request TransFem-org/sfm-js!5
This commit is contained in:
commit
eb43eb0298
12 changed files with 292 additions and 180 deletions
11
.eslintrc.js
11
.eslintrc.js
|
@ -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": "^_",
|
||||||
|
}]
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
@ -295,7 +316,7 @@ export function notMatch(parser: Parser<any>): Parser<null> {
|
||||||
* @returns A {@link Failure} object if `parserExcluded` succeeds, or if `parserIncluded` fails, and a {@link Success} object
|
* @returns A {@link Failure} object if `parserExcluded` succeeds, or if `parserIncluded` fails, and a {@link Success} object
|
||||||
* otherwise.
|
* otherwise.
|
||||||
*/
|
*/
|
||||||
export function difference(parserIncluded: Parser<any>, parserExcluded: Parser<any>): Parser<string> {
|
export function difference(parserIncluded: Parser<string>, parserExcluded: Parser<string>): Parser<string> {
|
||||||
return new Parser((input, index, state) => {
|
return new Parser((input, index, state) => {
|
||||||
const exclude = parserExcluded.handler(input, index, state);
|
const exclude = parserExcluded.handler(input, index, state);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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]));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
15
test/core.ts
15
test/core.ts
|
@ -1,28 +1,33 @@
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
import * as P from '../src/internal/core';
|
import * as P from '../src/internal/core';
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
nestLimit: 3,
|
||||||
|
depth: 0,
|
||||||
|
};
|
||||||
|
|
||||||
describe('core', () => {
|
describe('core', () => {
|
||||||
describe('difference', () => {
|
describe('difference', () => {
|
||||||
test('basic', () => {
|
test('basic', () => {
|
||||||
const parser = P.difference(P.regexp(/\p{Letter}/u), P.str('x'));
|
const parser = P.difference(P.regexp(/\p{Letter}/u), P.str('x'));
|
||||||
|
|
||||||
let result = parser.handler('x',0,{}) as P.Success<any>;
|
let result = parser.handler('x',0,state) as P.Success<any>;
|
||||||
assert.deepStrictEqual(result,P.failure());
|
assert.deepStrictEqual(result,P.failure());
|
||||||
|
|
||||||
result = parser.handler('a',0,{}) as P.Success<any>;
|
result = parser.handler('a',0,state) as P.Success<any>;
|
||||||
assert.deepStrictEqual(result,P.success(1,'a'));
|
assert.deepStrictEqual(result,P.success(1,'a'));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('horizontal whitespace', () => {
|
test('horizontal whitespace', () => {
|
||||||
const parser = P.difference(P.regexp(/\s/u), P.newline);
|
const parser = P.difference(P.regexp(/\s/u), P.newline);
|
||||||
|
|
||||||
let result = parser.handler('\n',0,{}) as P.Success<any>;
|
let result = parser.handler('\n',0,state) as P.Success<any>;
|
||||||
assert.deepStrictEqual(result,P.failure());
|
assert.deepStrictEqual(result,P.failure());
|
||||||
|
|
||||||
result = parser.handler(' ',0,{}) as P.Success<any>;
|
result = parser.handler(' ',0,state) as P.Success<any>;
|
||||||
assert.deepStrictEqual(result,P.success(1,' '));
|
assert.deepStrictEqual(result,P.success(1,' '));
|
||||||
|
|
||||||
result = parser.handler('\t',0,{}) as P.Success<any>;
|
result = parser.handler('\t',0,state) as P.Success<any>;
|
||||||
assert.deepStrictEqual(result,P.success(1,'\t'));
|
assert.deepStrictEqual(result,P.success(1,'\t'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
Loading…
Reference in a new issue