diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b89614f..0023da0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -18,7 +18,6 @@ "extensions": [ "denoland.vscode-deno", "esbenp.prettier-vscode", - "tombonnike.vscode-status-bar-format-toggle", "alefragnani.bookmarks", "xabikos.javascriptsnippets", "coenraads.bracket-pair-colorizer", diff --git a/.vscode/settings.json b/.vscode/settings.json index 105be8c..3ca6f1b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,14 +1,14 @@ { - "deno.enable": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "prettier.tabWidth": 4, - "prettier.useTabs": true, - "editor.formatOnPaste": false, - "editor.formatOnType": false, - "editor.formatOnSave": true, - "prettier.singleQuote": true, - "deno.import_intellisense_origins": { - "https://deno.land": true - }, - //"editor.formatOnSaveMode": "modifications", -} \ No newline at end of file + "deno.enable": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "prettier.tabWidth": 4, + "prettier.useTabs": true, + "prettier.singleQuote": true, + "editor.formatOnPaste": false, + "editor.formatOnType": false, + "editor.formatOnSave": true, + "deno.import_intellisense_origins": { + "https://deno.land": true + } + //"editor.formatOnSaveMode": "modifications", +} diff --git a/container/Dockerfile b/container/Dockerfile index 2fbfead..89cfedf 100644 --- a/container/Dockerfile +++ b/container/Dockerfile @@ -6,7 +6,9 @@ RUN usermod -u 1000 deno \ && groupmod -g 1000 deno \ && chown deno:deno /deno-dir/ \ && mkdir -p /home/deno \ + && mkdir -p /workspace \ && chown deno:deno /home/deno \ + && chown deno:deno /workspace \ && chmod 751 /home/deno \ && rm -rf /usr/local/bin/docker-entrypoint.sh \ # Install git @@ -20,7 +22,7 @@ RUN usermod -u 1000 deno \ USER deno # Install denon -RUN deno install -qAf --unstable https://deno.land/x/denon@2.4.4/denon.ts +RUN deno install -qAf --unstable https://deno.land/x/denon/denon.ts WORKDIR /workspace ENTRYPOINT [] diff --git a/src/server/MDParserArcsecond.ts b/src/server/MDParserArcsecond.ts index 772e51d..ff5d5c9 100644 --- a/src/server/MDParserArcsecond.ts +++ b/src/server/MDParserArcsecond.ts @@ -4,48 +4,322 @@ const { sequenceOf, str, many, + many1, digit, letter, char, anyChar, between, possibly, + whitespace, + recursiveParser, + anyCharExcept, + everyCharUntil, + skip, + pipeParsers, + tapParser, + lookAhead, + regex, } = Arcsecond; -const parseEscape = sequenceOf([char('\\'), anyChar]).map((x: any) => x[1]); -const parseText = many(choice([parseEscape, anyChar])).map((x: any) => - x.join('') -); -const _ParseHeader1 = sequenceOf([str('# ').map(() => '#'), parseText]); -const _ParseHeader2 = sequenceOf([str('## ').map(() => '##'), parseText]); -const _ParseHeader3 = sequenceOf([str('### ').map(() => '###'), parseText]); -const _ParseHeader4 = sequenceOf([str('#### ').map(() => '####'), parseText]); -const _ParseHeader5 = sequenceOf([str('##### ').map(() => '#####'), parseText]); -const _ParseHeader6 = sequenceOf([ - str('###### ').map(() => '######'), - parseText, +/// debug +const DebugOutput = (type: string) => (x: any) => ({ type: type, value: x }); + +/// Tokens (Only needed because we need the `as keyof iTOKEN` for type-script) +interface iTOKEN { + H1: any; + H2: any; + H3: any; + H4: any; + H5: any; + H6: any; + Bold1: any; + Bold2: any; + Italic1: any; + Italic2: any; + Code1: any; + Code2: any; +} + +const TOKEN: iTOKEN = { + H1: char('#'), + H2: str('##'), + H3: str('###'), + H4: str('####'), + H5: str('#####'), + H6: str('######'), + Bold1: str('**'), + Bold2: str('__'), + Italic1: char('*'), + Italic2: char('_'), + Code1: str('``'), + Code2: char('`'), +}; + +/// Builders +const _Header = (n: number) => { + const parser = TOKEN[`H${n}` as keyof iTOKEN]; + return pipeParsers([parser, char(' '), ParseText]).map( + DebugOutput(`h${n}`) + ); +}; + +const _CharEx = (ex: any) => + choice([ParseEscape, anyCharExcept(ex)]).map(DebugOutput('rawCharEx')); +const _StrEx = (ex: any) => + many1(choice([ParseEscape, anyCharExcept(ex)])) + .map((x: any) => x.join('')) + .map(DebugOutput('rawStrEx')); + +const _Between = (left: any) => (right: any) => (parse: any) => + sequenceOf([left, parse, right]).map((x: any) => x[1]); + +/// Helpers +const ParseTokens = choice([ + TOKEN.H6, + TOKEN.H5, + TOKEN.H4, + TOKEN.H3, + TOKEN.H2, + TOKEN.H1, + TOKEN.Bold1, + TOKEN.Bold2, + TOKEN.Italic1, + TOKEN.Italic2, + TOKEN.Code1, + TOKEN.Code2, ]); +const ParseEscape = sequenceOf([char('\\'), anyChar]).map((x: any) => x[1]); +const ParseText = many1(choice([ParseEscape, anyCharExcept(ParseTokens)])) + .map((x: any) => x.join('')) + .map(DebugOutput('text')); +const ParseRawChar = anyChar.map(DebugOutput('rawChar')); + +/// Parsers +const ParseH1 = _Header(1); +const ParseH2 = _Header(2); +const ParseH3 = _Header(3); +const ParseH4 = _Header(4); +const ParseH5 = _Header(5); +const ParseH6 = _Header(6); + +const ParseItalic = recursiveParser(() => { + const _Row = (edge: any) => { + const notSpace = lookAhead(regex(/^[^\s]/)); + const left = sequenceOf([edge, notSpace]); + const right = sequenceOf([notSpace, edge]); + return _Between(left)(right)( + many1(choice([ParseBold, ParseText, _CharEx(edge)])).map( + DebugOutput('italic') + ) + ); + }; + return choice([_Row(TOKEN.Italic1), _Row(TOKEN.Italic2)]); +}); + +const ParseBold = recursiveParser(() => { + const _Row = (edge: any) => { + const notSpace = lookAhead(regex(/^[^\s]/)); + const left = sequenceOf([edge, notSpace]); + const right = sequenceOf([notSpace, edge]); + return _Between(left)(right)( + many1(choice([ParseItalic, ParseText, _CharEx(edge)])).map( + DebugOutput('bold') + ) + ); + }; + return choice([_Row(TOKEN.Bold1), _Row(TOKEN.Bold2)]); +}); + +const ParseCode = recursiveParser(() => { + const _Row = (edge: any) => { + const left = edge; + const right = edge; + return _Between(left)(right)( + many1(choice([ParseText, _CharEx(edge)])).map(DebugOutput('code')) + ); + }; + return choice([_Row(TOKEN.Code1), _Row(TOKEN.Code2)]); +}); + +const ParseImage = recursiveParser(() => { + const imgAlt = ((left: any) => (right: any) => { + return pipeParsers([ + char('!'), + _Between(left)(right)(_StrEx(right)), + ]).map(DebugOutput('imgAlt')); + })(char('['))(char(']')); + + const imgURI = ((left: any) => (right: any) => { + const rightURI = choice([ + sequenceOf([char(' '), lookAhead(char('"'))]), + right, + ]); + return sequenceOf([ + _Between(left)(rightURI)(_StrEx(rightURI)).map( + DebugOutput('imgURI') + ), + possibly(_Between(char('"'))(char('"'))(_StrEx(char('"')))).map( + DebugOutput('imgTitle') + ), + ]); + })(char('('))(char(')')); -const _MDParser = choice([ - _ParseHeader6, - _ParseHeader5, - _ParseHeader4, - _ParseHeader3, - _ParseHeader2, - _ParseHeader1, + const imgID = ((left: any) => (right: any) => { + return _Between(left)(right)(_StrEx(right)).map(DebugOutput('imgID')); + })(choice([str(' ['), char('[')]))(char(']')); + + return sequenceOf([imgAlt, choice([imgURI, imgID])]).map( + DebugOutput('img') + ); +}); + +const ParseLink = recursiveParser(() => { + const linkText = ((left: any) => (right: any) => { + return _Between(left)(right)(_StrEx(right)).map( + DebugOutput('linkText') + ); + })(char('['))(char(']')); + + const linkURI = ((left: any) => (right: any) => { + const rightURI = choice([ + sequenceOf([char(' '), lookAhead(char('"'))]), + right, + ]); + return sequenceOf([ + _Between(left)(rightURI)(_StrEx(rightURI)).map( + DebugOutput('linkURI') + ), + possibly(_Between(char('"'))(char('"'))(_StrEx(char('"')))).map( + DebugOutput('linkTitle') + ), + ]); + })(char('('))(char(')')); + + const linkID = ((left: any) => (right: any) => { + return _Between(left)(right)(_StrEx(right)).map(DebugOutput('imgID')); + })(choice([str(' ['), char('[')]))(char(']')); + + return sequenceOf([linkText, choice([linkURI, linkID])]).map( + DebugOutput('link') + ); +}); + +const ParseReference = str('TO-DO'); + +/// Markdown Parser +const MDParser = choice([ + ParseH6, + ParseH5, + ParseH4, + ParseH3, + ParseH2, + ParseH1, + ParseBold, + ParseItalic, + ParseCode, + ParseImage, + ParseLink, + ParseReference, + ParseText, + ParseRawChar, ]); -export const MDParser = _MDParser; +const FinalParser = many1(MDParser); /// Maybe using a promise is not the best options here, /// but it will allow to change the parse engine quite easy. export function RunParser(pText: string): Promise { return new Promise((resolve, reject) => { try { - const parsed = _MDParser.run(pText); + const parsed = FinalParser.run(pText); resolve(parsed); } catch (ex) { reject(ex); } }); } + +const clearConsole = () => { + for (let index = 0; index < 50; index++) { + console.log(''); + } +}; + +const TEXT_LOG = true; +const testParse = (pString: string, pParser?: any) => { + const dumpObj = (obj: any) => + console.log(JSON.stringify(obj, void 0, ' ')); + const parsed = (pParser ?? FinalParser).run(pString); + const { isError, result } = parsed; + const lineLen = 80; + console.log(`${pString} ${'-'.repeat(lineLen - (pString.length + 1))}`); + if (!isError) { + if (!TEXT_LOG) { + console.log(result); + } else { + dumpObj(parsed); + } + } else { + console.log('ERROR: '); + dumpObj(parsed); + } + console.log(`${'-'.repeat(lineLen)}`); +}; + +clearConsole(); +// testParse('# Header 1'); +// testParse('## Header 2'); +// testParse('### Header 3'); +// testParse('#### Header 4'); +// testParse('##### Header 5'); +// testParse('###### Header 6'); +// testParse('Simple text'); +// testParse('*italic text*'); +// testParse('_italic text_'); +// testParse('**bold text**'); +// testParse('__bold text__'); +// testParse('*sometext \\* test*'); +// testParse('*italic **bold** italic*'); +// testParse('**bold *italic* bold**'); +// testParse('__bold @ text__'); +// testParse('_italic @ text_'); +// testParse('__bold ~ text__'); +// testParse('_italic £ text_'); +// testParse('_italic \\* text_'); +// testParse('_italic \\_ text_'); +// testParse('#test'); +// testParse('test #test'); +// testParse('_test #test_'); +// testParse('`code here`'); +// testParse('``code here``'); +// testParse('``code ` here``'); +// testParse('text *italic* **bold** ``code ` here`` __bold__'); +// testParse('![Alt text][id]'); +// testParse('![Alt text] [id]'); +// testParse('![Alt text](/path/to/img.jpg)'); +// testParse('![Alt text](/path/to/img.jpg "Optional title")'); +// testParse('![Alt \\[ \\] text][id]'); +// testParse('![Alt \\[ \\] text] [id]'); +// testParse('![Alt \\[ \\] text](/path/to/img.jpg)'); +// testParse('![Alt \\[ \\] text](/path/to/img.jpg "Optional title")'); +// testParse('![Alt text][i \\[ \\] d]'); +// testParse('![Alt text] [i \\[ \\] d]'); +// testParse('![Alt text](/path/\\[\\ \\]/img.jpg)'); +// testParse('![Alt text](/path/\\[\\ \\]/img.jpg "Optional title")'); +// testParse('![Alt text](/path/to/img.jpg "Optional \\[ \\] title")'); +// testParse('[an example][id]', ParseLink); +// testParse('[This link](http://example.net/)', ParseLink); +// testParse('[an example](http://example.com/ "Title")', ParseLink); + +testParse('[id]: url/to/image', ParseReference); +testParse('[id]: url/to/image "Optional title attribute"', ParseReference); +// testParse('[id]: url/to/image', ParseReference); +// testParse('[id]: url/to/image', ParseReference); +// testParse('[id]: url/to/image', ParseReference); +// testParse('[id]: url/to/image', ParseReference); +// testParse('[id]: url/to/image "Optional title attribute"', ParseReference); +// testParse('[id]: url/to/image "Optional title attribute"', ParseReference); +// testParse('[id]: url/to/image "Optional title attribute"', ParseReference); +// testParse('[id]: url/to/image "Optional title attribute"', ParseReference); +// testParse('[id]: url/to/image "Optional title attribute"', ParseReference);