mirror of
https://activitypub.software/TransFem-org/sfm-js
synced 2024-11-25 15:35:13 +00:00
Merge branch 'parser-documentation' into 'develop'
Added documentation See merge request TransFem-org/sfm-js!3
This commit is contained in:
commit
3b34b53b31
3 changed files with 407 additions and 0 deletions
|
@ -2,18 +2,37 @@
|
||||||
// Parsimmon-like stateful parser combinators
|
// Parsimmon-like stateful parser combinators
|
||||||
//
|
//
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds the information from a successful parse.
|
||||||
|
*/
|
||||||
export type Success<T> = {
|
export type Success<T> = {
|
||||||
success: true;
|
success: true;
|
||||||
value: T;
|
value: T;
|
||||||
index: number;
|
index: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a failed parse.
|
||||||
|
*/
|
||||||
export type Failure = { success: false };
|
export type Failure = { success: false };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Possible results from a parse.
|
||||||
|
*/
|
||||||
export type Result<T> = Success<T> | Failure;
|
export type Result<T> = Success<T> | Failure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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: any) => Result<T>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that always returns a parse success.
|
||||||
|
*
|
||||||
|
* @param index The index of the success.
|
||||||
|
* @param value The value of the success.
|
||||||
|
* @returns A {@link Success} object.
|
||||||
|
*/
|
||||||
export function success<T>(index: number, value: T): Success<T> {
|
export function success<T>(index: number, value: T): Success<T> {
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
@ -22,10 +41,18 @@ export function success<T>(index: number, value: T): Success<T> {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that always returns a parse failure.
|
||||||
|
*
|
||||||
|
* @returns A {@link Failure} object.
|
||||||
|
*/
|
||||||
export function failure(): Failure {
|
export function failure(): Failure {
|
||||||
return { success: false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parser class.
|
||||||
|
*/
|
||||||
export class Parser<T> {
|
export class Parser<T> {
|
||||||
public name?: string;
|
public name?: string;
|
||||||
public handler: ParserHandler<T>;
|
public handler: ParserHandler<T>;
|
||||||
|
@ -50,6 +77,13 @@ export class Parser<T> {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A method that maps the result of the parse with the provided function if successful, and returns a {@link Failure}
|
||||||
|
* otherwise.
|
||||||
|
*
|
||||||
|
* @param fn The function used to map the output of the parser.
|
||||||
|
* @returns The result of the parser mapped with `fn`.
|
||||||
|
*/
|
||||||
map<U>(fn: (value: T) => U): Parser<U> {
|
map<U>(fn: (value: T) => U): Parser<U> {
|
||||||
return new Parser((input, index, state) => {
|
return new Parser((input, index, state) => {
|
||||||
const result = this.handler(input, index, state);
|
const result = this.handler(input, index, state);
|
||||||
|
@ -60,6 +94,12 @@ export class Parser<T> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A method that returns the portion of the input that matches this {@link Parser Parser's} language and a {@link Failure}
|
||||||
|
* if the parse failed.
|
||||||
|
*
|
||||||
|
* @returns The plaintext related to the successful parse, and a {@link Failure} if the parse failed.
|
||||||
|
*/
|
||||||
text(): Parser<string> {
|
text(): Parser<string> {
|
||||||
return new Parser((input, index, state) => {
|
return new Parser((input, index, state) => {
|
||||||
const result = this.handler(input, index, state);
|
const result = this.handler(input, index, state);
|
||||||
|
@ -71,6 +111,12 @@ export class Parser<T> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A method that returns a {@link Parser} that matches at least `min` repetitions of this parser.
|
||||||
|
*
|
||||||
|
* @param min The minimum amount of times this parse must succeed to return a {@link Success}.
|
||||||
|
* @returns A Parser that returns a {@link Success} object it matches enough times, and a {@link Failure} otherwise.
|
||||||
|
*/
|
||||||
many(min: number): Parser<T[]> {
|
many(min: number): Parser<T[]> {
|
||||||
return new Parser((input, index, state) => {
|
return new Parser((input, index, state) => {
|
||||||
let result;
|
let result;
|
||||||
|
@ -91,6 +137,14 @@ export class Parser<T> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A method that returns a new {@link Parser} that matches at least `min` times, with each repetition separated
|
||||||
|
* by `separator`.
|
||||||
|
*
|
||||||
|
* @param separator The parser representing the separator that must appear between this parser's value.
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
sep(separator: Parser<any>, min: number): Parser<T[]> {
|
sep(separator: Parser<any>, 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.');
|
||||||
|
@ -104,6 +158,12 @@ export class Parser<T> {
|
||||||
]).map(result => [result[0], ...result[1]]);
|
]).map(result => [result[0], ...result[1]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A method that returns a new {@link Parser} that attempts to match, but returns a {@link Success} with the value `null`
|
||||||
|
* on failure.
|
||||||
|
*
|
||||||
|
* @returns A {@link Success} object.
|
||||||
|
*/
|
||||||
option<T>(): Parser<T | null> {
|
option<T>(): Parser<T | null> {
|
||||||
return alt([
|
return alt([
|
||||||
this,
|
this,
|
||||||
|
@ -112,6 +172,12 @@ export class Parser<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that returns a {@link Parser} that succeeds if it matches the supplied string.
|
||||||
|
*
|
||||||
|
* @param value The string that the returned {@link Parser} checks for.
|
||||||
|
* @returns A {@link Parser} that matches the supplied string.
|
||||||
|
*/
|
||||||
export function str<T extends string>(value: T): Parser<T> {
|
export function str<T extends string>(value: T): Parser<T> {
|
||||||
return new Parser((input, index, _state) => {
|
return new Parser((input, index, _state) => {
|
||||||
if ((input.length - index) < value.length) {
|
if ((input.length - index) < value.length) {
|
||||||
|
@ -124,6 +190,12 @@ export function str<T extends string>(value: T): Parser<T> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that returns {@link Parser} that succeeds if the input matches the supplied regular expression.
|
||||||
|
*
|
||||||
|
* @param pattern The regular expression that the returned {@link Parser} tries to match.
|
||||||
|
* @returns A {@link Parser} that checks if the input matches the supplied regular expression.
|
||||||
|
*/
|
||||||
export function regexp<T extends RegExp>(pattern: T): Parser<string> {
|
export function regexp<T extends RegExp>(pattern: T): Parser<string> {
|
||||||
const re = RegExp(`^(?:${pattern.source})`, pattern.flags);
|
const re = RegExp(`^(?:${pattern.source})`, pattern.flags);
|
||||||
return new Parser((input, index, _state) => {
|
return new Parser((input, index, _state) => {
|
||||||
|
@ -136,6 +208,20 @@ export function regexp<T extends RegExp>(pattern: T): Parser<string> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* The value in the {@link Success} returned by the parser varies depending on the value of `select`:
|
||||||
|
*
|
||||||
|
* If select is `null`, then the array of the results of the supplied parsers is returned, otherwise the value
|
||||||
|
* in the array at the specified index is returned.
|
||||||
|
*
|
||||||
|
* @param parsers The array of {@link Parser Parsers} that are checked to see if it succeeds.
|
||||||
|
* @param select The index of the result array that is returned.
|
||||||
|
* @returns A {@link Parser} that runs through the parsers in the order that they were provided and returns
|
||||||
|
* a value based on the state of `select` (the entire array if `null`, else the value held at the
|
||||||
|
* index specified by `select`).
|
||||||
|
*/
|
||||||
export function seq(parsers: Parser<any>[], select?: number): Parser<any> {
|
export function seq(parsers: Parser<any>[], select?: number): Parser<any> {
|
||||||
return new Parser((input, index, state) => {
|
return new Parser((input, index, state) => {
|
||||||
let result;
|
let result;
|
||||||
|
@ -153,6 +239,13 @@ export function seq(parsers: Parser<any>[], select?: number): Parser<any> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that returns a {@link Parser} that goes through the parsers provided, in order, and checks if any succeed.
|
||||||
|
* The returned parser produces the result of the first element of `parsers` to succeed, or a failure if none do.
|
||||||
|
*
|
||||||
|
* @param parsers The {@link Parser Parsers} that should be used.
|
||||||
|
* @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: Parser<any>[]): Parser<any> {
|
||||||
return new Parser((input, index, state) => {
|
return new Parser((input, index, state) => {
|
||||||
let result;
|
let result;
|
||||||
|
@ -166,12 +259,24 @@ export function alt(parsers: Parser<any>[]): Parser<any> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that returns a constant {@link Parser}.
|
||||||
|
*
|
||||||
|
* @param value The value to be used in the returned {@link Success} object.
|
||||||
|
* @returns A {@link Parser} that always returns a {@link Success} with the specified value.
|
||||||
|
*/
|
||||||
function succeeded<T>(value: T): Parser<T> {
|
function succeeded<T>(value: T): Parser<T> {
|
||||||
return new Parser((_input, index, _state) => {
|
return new Parser((_input, index, _state) => {
|
||||||
return success(index, value);
|
return success(index, value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that returns a {@link Parser} that inverts the result of the parser supplied.
|
||||||
|
*
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
export function notMatch(parser: Parser<any>): Parser<null> {
|
export function notMatch(parser: Parser<any>): 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);
|
||||||
|
@ -181,6 +286,15 @@ export function notMatch(parser: Parser<any>): Parser<null> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that returns a {@link Parser} that fails if `parserExcluded` succeeds, and returns the result of `parserIncluded`
|
||||||
|
* otherwise.
|
||||||
|
*
|
||||||
|
* @param parserIncluded The {@link Parser} that should succeed
|
||||||
|
* @param parserExcluded The {@link Parser} that should fail
|
||||||
|
* @returns A {@link Failure} object if `parserExcluded` succeeds, or if `parserIncluded` fails, and a {@link Success} object
|
||||||
|
* otherwise.
|
||||||
|
*/
|
||||||
export function difference(parserIncluded: Parser<any>, parserExcluded: Parser<any>): Parser<string> {
|
export function difference(parserIncluded: Parser<any>, parserExcluded: Parser<any>): 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);
|
||||||
|
@ -193,11 +307,19 @@ export function difference(parserIncluded: Parser<any>, parserExcluded: Parser<a
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A {@link Parser} that matches the carriage return character `\r`. */
|
||||||
export const cr = str('\r');
|
export const cr = str('\r');
|
||||||
|
/** A {@link Parser} that matches the line feed character `\n`. */
|
||||||
export const lf = str('\n');
|
export const lf = str('\n');
|
||||||
|
/** A {@link Parser} that matches the character sequence `\r\n`. */
|
||||||
export const crlf = str('\r\n');
|
export const crlf = str('\r\n');
|
||||||
|
/** A {@link Parser} that matches for any valid new line sequences. */
|
||||||
export const newline = alt([crlf, cr, lf]);
|
export const newline = alt([crlf, cr, lf]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Parser} that succeeds so long as it is not at the end of the input string and returns the value of
|
||||||
|
* the next character.
|
||||||
|
*/
|
||||||
export const char = new Parser((input, index, _state) => {
|
export const char = new Parser((input, index, _state) => {
|
||||||
if ((input.length - index) < 1) {
|
if ((input.length - index) < 1) {
|
||||||
return failure();
|
return failure();
|
||||||
|
@ -206,6 +328,10 @@ export const char = new Parser((input, index, _state) => {
|
||||||
return success(index + 1, value);
|
return success(index + 1, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Parser} that checks that the current position is the beginning of a line. For this parser to succeed,
|
||||||
|
* either the current index must be zero, or the previous character is a `\n` or `\r`.
|
||||||
|
*/
|
||||||
export const lineBegin = new Parser((input, index, state) => {
|
export const lineBegin = new Parser((input, index, state) => {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
return success(index, null);
|
return success(index, null);
|
||||||
|
@ -219,6 +345,10 @@ export const lineBegin = new Parser((input, index, state) => {
|
||||||
return failure();
|
return failure();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link Parser} that checks that the current position is the end of a line. For this parser to succeed, either the current
|
||||||
|
* index must be equal to the input length, or the current character is a `\n` or `\r`.
|
||||||
|
*/
|
||||||
export const lineEnd = new Parser((input, index, state) => {
|
export const lineEnd = new Parser((input, index, state) => {
|
||||||
if (index === input.length) {
|
if (index === input.length) {
|
||||||
return success(index, null);
|
return success(index, null);
|
||||||
|
@ -232,6 +362,12 @@ export const lineEnd = new Parser((input, index, state) => {
|
||||||
return failure();
|
return failure();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that lazily loads the supplied {@link Parser}
|
||||||
|
*
|
||||||
|
* @param fn The {@link Parser} that the returned parser should use.
|
||||||
|
* @returns A {@link Parser} that checks using the supplied parser's {@link Parser.handler}
|
||||||
|
*/
|
||||||
export function lazy<T>(fn: () => Parser<T>): Parser<T> {
|
export function lazy<T>(fn: () => Parser<T>): Parser<T> {
|
||||||
const parser: Parser<T> = new Parser((input, index, state) => {
|
const parser: Parser<T> = new Parser((input, index, state) => {
|
||||||
parser.handler = fn().handler;
|
parser.handler = fn().handler;
|
||||||
|
|
|
@ -3,10 +3,21 @@ import { language } from './parser';
|
||||||
import { mergeText } from './util';
|
import { mergeText } from './util';
|
||||||
import * as P from './core';
|
import * as P from './core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type representing the available options for the full parser.
|
||||||
|
*/
|
||||||
export type FullParserOpts = {
|
export type FullParserOpts = {
|
||||||
nestLimit?: number;
|
nestLimit?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that parses through the input text with the full parser and returns the AST representing the
|
||||||
|
* result.
|
||||||
|
*
|
||||||
|
* @param input The input string to parse.
|
||||||
|
* @param opts The options used for the parsing.
|
||||||
|
* @returns An array of nodes representing the resulting styles.
|
||||||
|
*/
|
||||||
export function fullParser(input: string, opts: FullParserOpts): M.MfmNode[] {
|
export function fullParser(input: string, opts: FullParserOpts): M.MfmNode[] {
|
||||||
const result = language.fullParser.handler(input, 0, {
|
const result = language.fullParser.handler(input, 0, {
|
||||||
nestLimit: (opts.nestLimit != null) ? opts.nestLimit : 20,
|
nestLimit: (opts.nestLimit != null) ? opts.nestLimit : 20,
|
||||||
|
@ -17,6 +28,13 @@ export function fullParser(input: string, opts: FullParserOpts): M.MfmNode[] {
|
||||||
return mergeText(result.value);
|
return mergeText(result.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that parses through the input text with the simple parser and returns the AST representing the
|
||||||
|
* result.
|
||||||
|
*
|
||||||
|
* @param input The input string to parse.
|
||||||
|
* @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, { }) as P.Success<any>;
|
||||||
return mergeText(result.value);
|
return mergeText(result.value);
|
||||||
|
|
|
@ -12,10 +12,25 @@ import twemojiRegex from '@twemoji/parser/dist/lib/regex';
|
||||||
type ArgPair = { k: string, v: string | true };
|
type ArgPair = { k: string, v: string | true };
|
||||||
type Args = Record<string, string | true>;
|
type Args = Record<string, string | true>;
|
||||||
|
|
||||||
|
/** A {@link P.Parser Parser} that matches all whitespace characters other than ones representing new lines */
|
||||||
const space = P.difference(P.regexp(/\s/), P.newline);
|
const space = P.difference(P.regexp(/\s/), P.newline);
|
||||||
|
/** A {@link P.Parser Parser} that matches all alphanumeric characters */
|
||||||
const alphaAndNum = P.regexp(/\p{Letter}|\p{Number}/iu);
|
const alphaAndNum = P.regexp(/\p{Letter}|\p{Number}/iu);
|
||||||
|
/** A {@link P.Parser Parser} that matches any valid new line sequences */
|
||||||
const newLine = P.alt([P.crlf, P.cr, P.lf]);
|
const newLine = P.alt([P.crlf, P.cr, P.lf]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that returns a {@link P.Parser Parser} which goes through the supplied parsers sequentially.
|
||||||
|
* If the first provided parser fails, a {@link P.Failure Failure} object is returned, the matched portion of the
|
||||||
|
* input text is returned if any other parsers fail, and an array of the values of the parsers'
|
||||||
|
* {@link P.Success Successes}.
|
||||||
|
*
|
||||||
|
* @param parsers The list of {@link P.Parser Parsers} to use.
|
||||||
|
* @returns A {@link P.Parser Parser} that goes through the supplied parsers sequentially and makes sure that each
|
||||||
|
* succeeds. If the first parser fails, a {@link P.Failure Failure} object is returned. If any other parser
|
||||||
|
* fails, the matched portion of the input text is returned. If all succeed, the array of each
|
||||||
|
* {@link P.Success Success'} value is returned.
|
||||||
|
*/
|
||||||
function seqOrText(parsers: P.Parser<any>[]): P.Parser<any[] | string> {
|
function seqOrText(parsers: P.Parser<any>[]): P.Parser<any[] | string> {
|
||||||
return new P.Parser<any[] | string>((input, index, state) => {
|
return new P.Parser<any[] | string>((input, index, state) => {
|
||||||
const accum: any[] = [];
|
const accum: any[] = [];
|
||||||
|
@ -36,18 +51,34 @@ function seqOrText(parsers: P.Parser<any>[]): P.Parser<any[] | string> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that succeeds if the supplied state is not a link label.
|
||||||
|
*/
|
||||||
const notLinkLabel = new P.Parser((_input, index, state) => {
|
const notLinkLabel = new P.Parser((_input, index, state) => {
|
||||||
return (!state.linkLabel)
|
return (!state.linkLabel)
|
||||||
? P.success(index, null)
|
? P.success(index, null)
|
||||||
: P.failure();
|
: P.failure();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that succeeds if the current nest depth is less than the nest limit.
|
||||||
|
*/
|
||||||
const nestable = new P.Parser((_input, index, state) => {
|
const nestable = new P.Parser((_input, index, state) => {
|
||||||
return (state.depth < state.nestLimit)
|
return (state.depth < state.nestLimit)
|
||||||
? P.success(index, null)
|
? P.success(index, null)
|
||||||
: P.failure();
|
: P.failure();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that returns a {@link P.Parser Parser} that calls `parser` if it is possible to nest, and `fallback`
|
||||||
|
* otherwise.
|
||||||
|
*
|
||||||
|
* @param parser The {@link P.Parser Parser} that gets used if it is currently possible to nest.
|
||||||
|
* @param fallback The {@link P.Parser Parser} that gets used if it is not currently possible to nest.
|
||||||
|
* If `null`, {@link P.char} is used.
|
||||||
|
* @returns A {@link P.Parser Parser} that checks that it is possible to nest, returning the result of `parser` if it is,
|
||||||
|
* otherwise returning the result of `fallback`
|
||||||
|
*/
|
||||||
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([
|
||||||
|
@ -62,15 +93,37 @@ function nest<T>(parser: P.Parser<T>, fallback?: P.Parser<string>): P.Parser<T |
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The collection of {@link P.Parser Parsers} representing the rules that make up SFM
|
||||||
|
*/
|
||||||
export const language = P.createLanguage({
|
export const language = P.createLanguage({
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches every SFM rule as many times as possible.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns A {@link P.Parser Parser} that matches every rule as many times as possible
|
||||||
|
*/
|
||||||
fullParser: r => {
|
fullParser: r => {
|
||||||
return r.full.many(0);
|
return r.full.many(0);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches {@link language.unicodeEmoji}, {@link language.emojiCode},
|
||||||
|
* and {@link language.text} as many times as possible
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns A {@link P.Parser Parser} that matches very few SFM rules as many times as possible.
|
||||||
|
*/
|
||||||
simpleParser: r => {
|
simpleParser: r => {
|
||||||
return r.simple.many(0);
|
return r.simple.many(0);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches any SFM rule.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns A {@link P.Parser Parser} that matches any rule.
|
||||||
|
*/
|
||||||
full: r => {
|
full: r => {
|
||||||
return P.alt([
|
return P.alt([
|
||||||
// Regexp
|
// Regexp
|
||||||
|
@ -129,6 +182,13 @@ export const language = P.createLanguage({
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that only matches {@link language.unicodeEmoji}, {@link language.emojiCode},
|
||||||
|
* and {@link language.text}.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns A {@link P.Parser Parser} that matches very few SFM rules.
|
||||||
|
*/
|
||||||
simple: r => {
|
simple: r => {
|
||||||
return P.alt([
|
return P.alt([
|
||||||
r.unicodeEmoji, // Regexp
|
r.unicodeEmoji, // Regexp
|
||||||
|
@ -137,6 +197,12 @@ export const language = P.createLanguage({
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that only matches rules that can be done without disrupting text.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns A {@link P.Parser Parser} that matches all inline rules.
|
||||||
|
*/
|
||||||
inline: r => {
|
inline: r => {
|
||||||
return P.alt([
|
return P.alt([
|
||||||
// Regexp
|
// Regexp
|
||||||
|
@ -185,6 +251,12 @@ export const language = P.createLanguage({
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches single lines that begin with a `>` character.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @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('>'),
|
||||||
|
@ -222,6 +294,14 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is surrounded by the ` ``` ` mark.
|
||||||
|
* Text directly after the opening mark decides the language that the syntax highlighting uses.
|
||||||
|
* The marks must be on their own lines.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
codeBlock: r => {
|
codeBlock: r => {
|
||||||
const mark = P.str('```');
|
const mark = P.str('```');
|
||||||
return P.seq([
|
return P.seq([
|
||||||
|
@ -242,6 +322,13 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is opened with `\[` and closed with `\]`.
|
||||||
|
* The marks do not have to be on their own lines, nor do they need to be the first.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
mathBlock: r => {
|
mathBlock: r => {
|
||||||
const open = P.str('\\[');
|
const open = P.str('\\[');
|
||||||
const close = P.str('\\]');
|
const close = P.str('\\]');
|
||||||
|
@ -259,6 +346,14 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is opened with `<center>` and closed with `</center>`.
|
||||||
|
* The marks do not have to be on their own lines.
|
||||||
|
* The opening mark must appear at the beginning of its line, and the closing mark must appear at the end of its line.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
centerTag: r => {
|
centerTag: r => {
|
||||||
const open = P.str('<center>');
|
const open = P.str('<center>');
|
||||||
const close = P.str('</center>');
|
const close = P.str('</center>');
|
||||||
|
@ -277,6 +372,13 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is surrounded with the `***` mark.
|
||||||
|
* The marks do not have to be on their own lines and have no restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
big: r => {
|
big: r => {
|
||||||
const mark = P.str('***');
|
const mark = P.str('***');
|
||||||
return seqOrText([
|
return seqOrText([
|
||||||
|
@ -289,6 +391,13 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is surrounded with the `**` mark.
|
||||||
|
* The marks do not have to be on their own lines and have no restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
boldAsta: r => {
|
boldAsta: r => {
|
||||||
const mark = P.str('**');
|
const mark = P.str('**');
|
||||||
return seqOrText([
|
return seqOrText([
|
||||||
|
@ -301,6 +410,13 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is opened with `<b>` and closed with `</b>`.
|
||||||
|
* The marks do not have to be on their own lines and have no restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
boldTag: r => {
|
boldTag: r => {
|
||||||
const open = P.str('<b>');
|
const open = P.str('<b>');
|
||||||
const close = P.str('</b>');
|
const close = P.str('</b>');
|
||||||
|
@ -314,6 +430,13 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is surrounded with the `__` mark.
|
||||||
|
* The marks do not have to be on their own lines and have no restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
boldUnder: r => {
|
boldUnder: r => {
|
||||||
const mark = P.str('__');
|
const mark = P.str('__');
|
||||||
return P.seq([
|
return P.seq([
|
||||||
|
@ -323,6 +446,13 @@ export const language = P.createLanguage({
|
||||||
]).map(result => M.BOLD(mergeText(result[1] as string[])));
|
]).map(result => M.BOLD(mergeText(result[1] as string[])));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is opened with `<small>` and closed with `</small>`.
|
||||||
|
* The marks do not have to be on their own lines and have no restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
smallTag: r => {
|
smallTag: r => {
|
||||||
const open = P.str('<small>');
|
const open = P.str('<small>');
|
||||||
const close = P.str('</small>');
|
const close = P.str('</small>');
|
||||||
|
@ -336,6 +466,13 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is opened with `<i>` and closed with `</i>`.
|
||||||
|
* The marks do not have to be on their own lines and have no restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
italicTag: r => {
|
italicTag: r => {
|
||||||
const open = P.str('<i>');
|
const open = P.str('<i>');
|
||||||
const close = P.str('</i>');
|
const close = P.str('</i>');
|
||||||
|
@ -349,6 +486,13 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is surrounded with the `*` mark.
|
||||||
|
* The marks do not have to be on their own lines and have no restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
italicAsta: r => {
|
italicAsta: r => {
|
||||||
const mark = P.str('*');
|
const mark = P.str('*');
|
||||||
const parser = P.seq([
|
const parser = P.seq([
|
||||||
|
@ -370,6 +514,13 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is surrounded with the `_` mark.
|
||||||
|
* The marks do not have to be on their own lines and have no restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
italicUnder: r => {
|
italicUnder: r => {
|
||||||
const mark = P.str('_');
|
const mark = P.str('_');
|
||||||
const parser = P.seq([
|
const parser = P.seq([
|
||||||
|
@ -391,6 +542,13 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is opened with `<s>` and closed with `</s>`.
|
||||||
|
* The marks do not have to be on their own lines and have no restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
strikeTag: r => {
|
strikeTag: r => {
|
||||||
const open = P.str('<s>');
|
const open = P.str('<s>');
|
||||||
const close = P.str('</s>');
|
const close = P.str('</s>');
|
||||||
|
@ -404,6 +562,13 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is surrounded with the `~~` mark.
|
||||||
|
* The marks do not have to be on their own lines and have no restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
strikeWave: r => {
|
strikeWave: r => {
|
||||||
const mark = P.str('~~');
|
const mark = P.str('~~');
|
||||||
return seqOrText([
|
return seqOrText([
|
||||||
|
@ -416,11 +581,24 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches unicode emojis according to a regex.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
unicodeEmoji: r => {
|
unicodeEmoji: r => {
|
||||||
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));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is opened with `<plain>` and closed with `</plain>`.
|
||||||
|
* The marks do not have to be on their own lines and have no restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
plainTag: r => {
|
plainTag: r => {
|
||||||
const open = P.str('<plain>');
|
const open = P.str('<plain>');
|
||||||
const close = P.str('</plain>');
|
const close = P.str('</plain>');
|
||||||
|
@ -483,6 +661,13 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches multi-line text that is surrounded with the `` ` `` mark.
|
||||||
|
* The marks do not have to be on their own lines and have no restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
inlineCode: r => {
|
inlineCode: r => {
|
||||||
const mark = P.str('`');
|
const mark = P.str('`');
|
||||||
return P.seq([
|
return P.seq([
|
||||||
|
@ -495,6 +680,13 @@ export const language = P.createLanguage({
|
||||||
]).map(result => M.INLINE_CODE(result[1].join('')));
|
]).map(result => M.INLINE_CODE(result[1].join('')));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches single-line text opened with `\(` and closed with `\)`.
|
||||||
|
* The marks must be on the same line, but have no other restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
mathInline: r => {
|
mathInline: r => {
|
||||||
const open = P.str('\\(');
|
const open = P.str('\\(');
|
||||||
const close = P.str('\\)');
|
const close = P.str('\\)');
|
||||||
|
@ -508,6 +700,14 @@ export const language = P.createLanguage({
|
||||||
]).map(result => M.MATH_INLINE(result[1].join('')));
|
]).map(result => M.MATH_INLINE(result[1].join('')));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches user mentions, which all must begin with `@{username}`, but only have to be followed by
|
||||||
|
* `@{hostname}` if the user is on a different instance.
|
||||||
|
* The mention must be contained within one line, but it has no other restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r the rules of SFM
|
||||||
|
* @returns The {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
mention: r => {
|
mention: r => {
|
||||||
const parser = P.seq([
|
const parser = P.seq([
|
||||||
notLinkLabel,
|
notLinkLabel,
|
||||||
|
@ -574,6 +774,15 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches single word hashtags starting with the character `#`.
|
||||||
|
* The contents of the hashtag are limited to alphanumeric characters, but cannot be made up of exclusively numbers.
|
||||||
|
* There are some characters that must be closed with another in order for the parse to succeed.
|
||||||
|
* There are no restrictions on the placement of hashtags.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns A {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
hashtag: r => {
|
hashtag: r => {
|
||||||
const mark = P.str('#');
|
const mark = P.str('#');
|
||||||
const hashTagChar = P.seq([
|
const hashTagChar = P.seq([
|
||||||
|
@ -620,6 +829,14 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches single word emojis surrounded by the `:` mark.
|
||||||
|
* The marks must be on the same line, and alphanumeric characters cannot appear both in front
|
||||||
|
* of, and behind, the opening and closing marks respectively.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns A {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
emojiCode: r => {
|
emojiCode: r => {
|
||||||
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(':');
|
||||||
|
@ -632,6 +849,16 @@ export const language = P.createLanguage({
|
||||||
], 2).map(name => M.EMOJI_CODE((name as string).normalize('NFC')));
|
], 2).map(name => M.EMOJI_CODE((name as string).normalize('NFC')));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches inline links, which are made up of the label and the url.
|
||||||
|
* The label must be opened with either `?[` for silent links or `[` for normal links and closed with `]`.
|
||||||
|
* The url must be opened with `(` and closed with `)`, and the contents of the url must follow either the
|
||||||
|
* {@link language.url} or {@link language.urlAlt} rules.
|
||||||
|
* The inline link must be on a single line, but has no other restrictions on placement.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns A {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
link: r => {
|
link: r => {
|
||||||
const labelInline = new P.Parser((input, index, state) => {
|
const labelInline = new P.Parser((input, index, state) => {
|
||||||
state.linkLabel = true;
|
state.linkLabel = true;
|
||||||
|
@ -659,6 +886,12 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches the standard format for urls.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns A {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
url: r => {
|
url: r => {
|
||||||
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<any> = P.lazy(() => P.alt([
|
||||||
|
@ -698,6 +931,12 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches an alternate form for urls, where it is opened with `<` and closed with `>`.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns A {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
urlAlt: r => {
|
urlAlt: r => {
|
||||||
const open = P.str('<');
|
const open = P.str('<');
|
||||||
const close = P.str('>');
|
const close = P.str('>');
|
||||||
|
@ -718,6 +957,14 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link P.Parser Parser} that matches single line text for a search query. The query must have either `[検索]` or
|
||||||
|
* `[search]` at the end of the line.
|
||||||
|
* The query and button must be on the same line and there can be nothing else on that line.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns A {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
search: r => {
|
search: r => {
|
||||||
const button = P.alt([
|
const button = P.alt([
|
||||||
P.regexp(/\[(検索|search)\]/i),
|
P.regexp(/\[(検索|search)\]/i),
|
||||||
|
@ -742,5 +989,11 @@ export const language = P.createLanguage({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A rule that simply collects characters.
|
||||||
|
*
|
||||||
|
* @param r The rules of SFM
|
||||||
|
* @returns A {@link P.Parser Parser} for this rule
|
||||||
|
*/
|
||||||
text: r => P.char,
|
text: r => P.char,
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue