update iroiro

This commit is contained in:
marihachi 2020-02-16 23:27:25 +09:00
parent 791d3f7665
commit 7cf838abda
16 changed files with 301 additions and 106 deletions

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2020 Marihachi
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,13 +1,27 @@
# mfm-parser-pegjs # mfm-parser-pegjs
## Description ## Description
A trial of creating a MFM parser with peg.js A MFM parser made with PEG.js (In developing)
## Installation ## Installation
``` ```
npm i npm i mfm-parser-pegjs
``` ```
## Build ## Usage
```ts
import { parse } from 'mfm-parser-pegjs';
// parse a MFM code
const result = parse('this is a ***MFM text***');
```
## Usage (Repository)
### 1. Clone
```
git clone https://github.com/marihachi/mfm-parser-pegjs.git
```
### 2. Build
For production: For production:
``` ```
npm run build npm run build
@ -18,7 +32,10 @@ For development:
npm run build-dev npm run build-dev
``` ```
## Start ### Use Interactive interface
``` ```
npm start npm parse
``` ```
## License
MIT

View file

@ -2,16 +2,17 @@
"private": true, "private": true,
"name": "mfm-parser-pegjs", "name": "mfm-parser-pegjs",
"version": "0.1.0", "version": "0.1.0",
"description": "A MFM parser made with PEG.js",
"main": "./built/index.js", "main": "./built/index.js",
"scripts": { "scripts": {
"build": "npm run peg && npm run tsc && npm run webpack", "build": "npm run tsc && npm run peg && npm run webpack",
"build-dev": "npm run peg-dev && npm run tsc && npm run webpack-dev", "build-dev": "npm run tsc && npm run peg-dev && npm run webpack-dev",
"peg": "mkdirp ./built/parser && pegjs -o built/parser/core-parser.js src/parser/core-parser.pegjs", "peg": "node ./built/build",
"peg-dev": "mkdirp ./built/parser && pegjs -o built/parser/core-parser.js --trace src/parser/core-parser.pegjs", "peg-dev": "node ./built/build trace",
"tsc": "tsc", "tsc": "tsc",
"webpack": "webpack --mode=production", "webpack": "webpack --mode=production",
"webpack-dev": "webpack --mode=development", "webpack-dev": "webpack --mode=development",
"start": "node ." "parse": "node ./built/parse"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -21,6 +22,7 @@
"devDependencies": { "devDependencies": {
"@types/node": "^12.0.4", "@types/node": "^12.0.4",
"@types/parsimmon": "^1.10.1", "@types/parsimmon": "^1.10.1",
"@types/pegjs": "^0.10.1",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"parsimmon": "^1.13.0", "parsimmon": "^1.13.0",
"pegjs": "^0.10.0", "pegjs": "^0.10.0",
@ -28,5 +30,10 @@
"typescript": "3.7.x", "typescript": "3.7.x",
"webpack": "4.40.x", "webpack": "4.40.x",
"webpack-cli": "3.3.x" "webpack-cli": "3.3.x"
} },
"files": [
"./built/index.js",
"./built/index.d.ts",
"./built/parser"
]
} }

25
src/build.ts Normal file
View file

@ -0,0 +1,25 @@
import { promises as fs } from 'fs';
import path from 'path';
import { generateCode } from './misc/generate-peg';
async function entryPoint() {
// get arguments
let trace = false;
if (process.argv.some(i => i == 'trace')) {
trace = true;
}
const srcPath = path.join(__dirname, '../src/parser/core-parser.pegjs');
const destPath = path.join(__dirname, '../built/parser/core-parser.js');
// generate a code from PEG
const generatedCode = await generateCode(srcPath, trace);
// write the generated code
await fs.writeFile(destPath, generatedCode, { encoding: 'utf8' });
}
entryPoint()
.catch(err => {
console.log(err);
process.exit(1);
});

View file

@ -1,13 +1,10 @@
import { PegParser } from '../parser/peg-parser'; import { parse } from '../../built/index';
async function entryPoint() { async function entryPoint() {
const coreParser: PegParser = require('../../built/parser/core-parser.js'); const result = parse('abc');
const input = '[hoge]';
console.log('parsing input:', input);
const result = coreParser.parse(input);
console.log('parsing result:');
console.log(JSON.stringify(result)); console.log(JSON.stringify(result));
} }
entryPoint() entryPoint()
.catch(err => console.log(err)); .catch(err => {
console.log(err);
});

View file

@ -1,8 +1 @@
import { PegParser } from './parser/peg-parser'; export * from './parser';
const coreParser: PegParser = require('./parser/core-parser');
const input = '[hoge]';
console.log('parsing input:', input);
const result = coreParser.parse(input);
console.log('parsing result:');
console.log(JSON.stringify(result));

32
src/misc/generate-peg.ts Normal file
View file

@ -0,0 +1,32 @@
import { promises as fs } from 'fs';
import peg from 'pegjs';
export async function generateParser(srcPath: string, trace?: boolean): Promise<peg.Parser>
{
// read the parser source
const source = await fs.readFile(srcPath, 'utf8');
// generate a parser code
const generatedCode = peg.generate(source, {
allowedStartRules: ['root', 'all', 'inline'],
trace: trace
});
return generatedCode;
}
export async function generateCode(srcPath: string, trace?: boolean): Promise<string>
{
// read the parser source
const source = await fs.readFile(srcPath, 'utf8');
// generate a parser code
const generatedCode = peg.generate(source, {
allowedStartRules: ['root', 'all', 'inline'],
output: 'source',
format: 'commonjs',
trace: trace
});
return generatedCode;
}

22
src/misc/inputLine.ts Normal file
View file

@ -0,0 +1,22 @@
import readLine from 'readline';
export class InputCanceledError extends Error {
constructor(message?: string) {
super(message);
}
}
export default function(message: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
const rl = readLine.createInterface(process.stdin, process.stdout);
rl.question(message, (ans) => {
rl.close();
resolve(ans);
});
rl.on('SIGINT', () => {
console.log('');
rl.close();
reject(new InputCanceledError('SIGINT interrupted'));
});
});
}

41
src/parse.ts Normal file
View file

@ -0,0 +1,41 @@
import inputLine, { InputCanceledError } from './misc/inputLine';
import { parse } from './parser/index';
async function entryPoint() {
console.log('intaractive parser');
while (true) {
let input: string;
try {
input = await inputLine('> ');
}
catch (err) {
if (err instanceof InputCanceledError) {
console.log('bye.');
return;
}
throw err;
}
// replace special chars
input = input
.replace(/\\n/g, '\n')
.replace(/\\t/g, '\t');
let result: any;
try {
result = parse(input);
console.log(JSON.stringify(result));
}
catch (err) {
console.log('parsing error:');
console.log(err);
}
console.log();
}
}
entryPoint()
.catch(err => {
console.log(err);
process.exit(1);
});

View file

@ -1,23 +1,23 @@
{ {
function buildList(head, others) { const {
return [ head, ...others ]; createTree,
} mergeText
} = require('./parser-utils');
function createTree(type, props, children) { function applyParser(input, rule) {
props = props || { }; let parseFunc = peg$parse;
children = children || [ ]; return parseFunc(input, rule ? { startRule : rule } : { });
children = !Array.isArray(children) ? [children] : children;
return {
node: { type, props },
children: children
};
} }
} }
root root
= block = ts:all*
/ inline {
return mergeText(ts);
}
all
= block / inline
// plain // plain
// = // =
@ -30,6 +30,7 @@ block
inline inline
= big = big
/ c:. { return createTree('text', { text: c }); }
// block: title // block: title
@ -38,68 +39,35 @@ title
= titleA / titleB = titleA / titleB
titleA titleA
= "【" content:titleA_content "】" = "【" content:(!("】" ENDLINE) i:inline { return i; })+ "】" ENDLINE
{ {
return createTree('title', { }, content); return createTree('title', { }, content);
} }
titleA_content
= (inline / titleA_text)+
titleA_text
= s:$(titleA_char+)
{
return createTree('text', { text: s });
}
titleA_char
= !(inline / "】") c:CHAR { return c; }
titleB titleB
= "[" content: titleB_content "]" = "[" content:(!("]" ENDLINE) i:inline { return i; })+ "]" ENDLINE
{ {
return createTree('title', { }, content); return createTree('title', { }, content);
} }
titleB_content
= (inline / titleB_text)+
titleB_text
= s:$(titleB_char+)
{
return createTree('text', { text: s });
}
titleB_char
= !(inline / "]") c:CHAR { return c; }
// block: quote // block: quote
// (handle the line as quote block if got a char ">" of the line head.)
quote quote
= head:quote_line tail:(NEWLINE tree:quote_line { return tree; })* = lines:quote_line+
{ {
const trees = [head, ...tail]; const children = applyParser(lines.join('\n'), 'root');
console.log(trees.map(tree => tree.children));//.flat(); return createTree('quote', { }, children);
return [head, ...tail].join('\n');
} }
quote_line quote_line
= ">" content:quote_content &ENDLINE { return createTree('quote', { }, content); } = ">" _? content:$(CHAR+) ENDLINE { return content; }
// TODO: allow nesting
quote_content
= quote_text
quote_text
= s:$(CHAR+) { return createTree('text', { text: s }); }
// block: search // block: search
search search
= q:search_query sp:[  \t] key:search_keyToken &ENDLINE = q:search_query sp:[  \t] key:search_keyToken ENDLINE
{ {
return createTree('search', { return createTree('search', {
query: q, query: q,
@ -120,29 +88,36 @@ search_keyToken
// block: blockCode // block: blockCode
blockCode blockCode
= "```" NEWLINE lines: (!("```" ENDLINE) line:blockCode_line NEWLINE { return line; } )* "```" &ENDLINE { return lines; } = "```" NEWLINE lines: (!("```" ENDLINE) line:blockCode_line NEWLINE { return line; } )* "```" ENDLINE { return lines; }
// TODO: allow nesting
blockCode_line blockCode_line
= t:$(CHAR*) { return t; } = (!"```" (block / inline))+
// inline: big // inline: big
big big
= "***" content:big_content "***" = "***" content:(!"***" i:inline { return i; })+ "***"
{ {
return createTree('big', { }, content); return createTree('big', { }, content);
} }
big_content
= (big_text / inline)*
big_text // inline: bold
= s:$(big_char+) { return createTree('text', { text: s }); }
big_char bold = bold_A / bold_B
= !("***") c:CHAR { return c; }
bold_A
= "**" content:(!"**" i:inline { return i; })+ "**"
{
return createTree('bold', { }, content);
}
bold_B
= "__" content:(!"__" i:inline { return i; })+ "__"
{
return createTree('bold', { }, content);
}
// Core rules // Core rules
@ -159,8 +134,5 @@ NEWLINE
EOF EOF
= !. = !.
// __ "whitespaces" _ "whitespace"
// = _+ = [ \t]
// _ "whitespace"
// = [ \t]

14
src/parser/index.ts Normal file
View file

@ -0,0 +1,14 @@
import peg from 'pegjs';
const coreParser: peg.Parser = require('./core-parser');
export interface Tree {
type: string;
props: Record<string, string>;
children: Tree[];
};
export function parse(input: string) {
let trees: Tree[];
trees = coreParser.parse(input);
return trees;
}

View file

@ -0,0 +1,60 @@
import { Tree } from '.';
export function createTree(type: string, props?: Record<string, any>, children?: Tree[]): Tree {
props = props || { };
children = children || [ ];
children = !Array.isArray(children) ? [children] : children;
return { type, props, children };
}
/**
* @param predicate specifies whether to group the previous item and the current item
* @returns grouped items
*/
export function groupContinuous<T>(arr: T[], predicate: (prev: T, current: T) => boolean): T[][] {
const dest: any[][] = [];
for (let i = 0; i < arr.length; i++) {
if (i != 0 && predicate(arr[i - 1], arr[i])) {
dest[dest.length - 1].push(arr[i]);
}
else {
dest.push([arr[i]]);
}
}
return dest;
}
export function mergeGroupedTrees(groupedTrees: Tree[][]): Tree[] {
return groupedTrees.reduce((acc, val) => acc.concat(val), ([] as Tree[]));
}
export function mergeText(trees: Tree[], recursive?: boolean): Tree[] {
let dest: Tree[];
let groupes: Tree[][];
// group trees
groupes = groupContinuous(trees, (prev, current) => prev.type == current.type);
// concatinate text
groupes = groupes.map((group) => {
if (group[0].type == 'text') {
return [
createTree('text', {
text: group.map(i => i.props.text).join('')
})
];
}
return group;
});
// merge groups
dest = mergeGroupedTrees(groupes);
return dest.map(tree => {
// apply recursively to children
return createTree(tree.type, tree.props, recursive ? mergeText(tree.children) : tree.children);
});
}

View file

@ -1,6 +0,0 @@
export interface PegParserOptions {
startRule?: string;
}
export interface PegParser {
parse(input: string, options?: PegParserOptions): any;
}

View file

@ -5,7 +5,7 @@
"module": "ESNext", "module": "ESNext",
"moduleResolution": "node", "moduleResolution": "node",
"outDir": "./built/client/", /* Redirect output structure to the directory. */ "outDir": "./built/client/", /* Redirect output structure to the directory. */
"rootDir": "./src/client/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "rootDir": "./src/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"removeComments": false, "removeComments": false,
/* Strict Type-Checking Options */ /* Strict Type-Checking Options */
@ -21,7 +21,7 @@
"experimentalDecorators": true, "experimentalDecorators": true,
}, },
"include": [ "include": [
"src/client/**/*", "src/**/*",
], ],
"exclude": [ "exclude": [
"node_modules", "node_modules",

View file

@ -3,7 +3,7 @@
/* Basic Options */ /* Basic Options */
"target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
//"declaration": true, /* Generates corresponding '.d.ts' file. */ "declaration": true, /* Generates corresponding '.d.ts' file. */
"outDir": "./built/", /* Redirect output structure to the directory. */ "outDir": "./built/", /* Redirect output structure to the directory. */
"rootDir": "./src/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ "rootDir": "./src/", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
"removeComments": true, /* Do not emit comments to output. */ "removeComments": true, /* Do not emit comments to output. */

View file

@ -21,6 +21,6 @@ module.exports = {
] ]
}, },
resolve: { resolve: {
extensions: ['.ts'], extensions: ['.ts', '.js'],
}, },
}; };