Changes
31 changed files (+1023/-982)
-
-
@@ -14,16 +14,4 @@ runs-on: ubuntu-lateststeps: - name: Checkout repository uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v2 with: node-version: '16' - name: Install dependencies run: yarn install - name: Build docs run: yarn docs:build - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: publish_dir: docs/.vuepress/dist github_token: ${{ secrets.GITHUB_TOKEN }} # TODO: Re-implement documentation website
-
-
-
@@ -9,11 +9,11 @@ runs-on: ubuntu-lateststeps: - name: Checkout repository uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v2 - name: Setup Deno uses: denoland/setup-deno@v1 with: node-version: '16' - name: Install dependencies run: yarn install deno-version: v1.26.x - name: Check source code format run: deno fmt --check - name: Lint files run: yarn lint run: deno lint
-
-
-
@@ -12,13 +12,18 @@ runs-on: ubuntu-lateststeps: - name: Checkout repository uses: actions/checkout@v3 - name: Setup Node.js uses: actions/setup-node@v2 - name: Setup Deno uses: denoland/setup-deno@v1 with: node-version: '16' - name: Install dependencies run: yarn install deno-version: v1.26.x - name: Run doc comment tests run: deno test --doc *.ts - name: Run tests run: yarn test run: deno test --coverage=coverage/ - name: Generate coverage report run: deno coverage coverage/ --lcov --output=cov.lcov - name: Upload coverage report uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v3 with: files: ./cov.lcov verbose: true
-
-
-
@@ -1,10 +1,5 @@node_modules lib coverage /npm /coverage *.lcov *.log yarn.lock package-lock.json docs/.vuepress/dist slack-message-parser-*.tgz
-
-
.nvmrc (deleted)
-
@@ -1,1 +0,0 @@v10.10.0
-
-
.tool-versions (new)
-
@@ -0,0 +1,1 @@deno 1.26.2
-
-
-
@@ -62,6 +62,5 @@ ### Fixed- Treat comma and dot as separator (Issue: [#4](https://github.com/pocka/slack-message-parser/issues/4), PR: [#5](https://github.com/pocka/slack-message-parser/pull/5)). [Unreleased]: https://github.com/pocka/slack-message-parser/compare/v2.0.2...HEAD [2.0.2]: https://github.com/pocka/slack-message-parser/compare/v2.0.1...v2.0.2 [2.0.2]: https://github.com/pocka/slack-message-parser/compare/v2.0.1...v2.0.2
-
-
-
@@ -22,9 +22,9 @@Usage with Typescript (recommended). ```ts import slackMessageParser, { Node, NodeType } from 'slack-message-parser' import slackMessageParser, { Node, NodeType } from "slack-message-parser"; const tree = slackMessageParser('Slack *message* ~to~ _parse_') const tree = slackMessageParser("Slack *message* ~to~ _parse_"); // tree is: // {
-
@@ -54,22 +54,22 @@ // Write your own!const toHTML = (node: Node): string => { switch (node.type) { case NodeType.Root: return `<p>${node.children.map(toHTML).join('')}</p>` return `<p>${node.children.map(toHTML).join("")}</p>`; case NodeType.Text: return node.text return node.text; case NodeType.Bold: return `<strong>${node.children.map(toHTML).join('')}</strong>` return `<strong>${node.children.map(toHTML).join("")}</strong>`; case NodeType.Italic: return `<i>${node.children.map(toHTML).join('')}</i>` return `<i>${node.children.map(toHTML).join("")}</i>`; case NodeType.Strike: return `<del>${node.children.map(toHTML).join('')}</del>` return `<del>${node.children.map(toHTML).join("")}</del>`; default: // You can use `source` property, which every nodes have, to serialize unknown nodes as-is return node.source return node.source; } } }; console.log(toHTML(tree)) console.log(toHTML(tree)); // Output: // '<p>Slack <strong>message</strong> <del>to</del> <i>parse</i></p>'
-
-
combinator.ts (new)
-
@@ -0,0 +1,65 @@import { Node } from "./types/Node.ts"; import { Parser, ParseText } from "./types/Parser.ts"; export const or = (parsers: Parser[]): Parser => { const { length } = parsers; return (text, position, rootParser) => { for (let i = 0; i < length; i++) { const match = parsers[i](text, position, rootParser); if (match) { return match; } } return null; }; }; export const regexp = ( pattern: RegExp, callback: ( match: string[], text: string, position: number, parseText: ParseText, ) => [Node, number] | null, ): Parser => (text, position, parseText) => { const match = text.substring(position).match(pattern); if (!match) { return null; } return callback(match, text, position, parseText); }; export const explicit = (parser: Parser): Parser => ( text, position, parseText, ) => { const prevChar = text.charAt(position - 1); if (prevChar && !prevChar.match(/[\s.,([{!?\-=]/)) { return null; } return parser(text, position, parseText); }; export const topOfLine = (parser: Parser): Parser => ( text, position, parseText, ) => { if (position > 0 && text.charAt(position - 1) !== "\n") { return null; } return parser(text, position, parseText); };
-
-
deno.json (new)
-
@@ -0,0 +1,23 @@{ "compilerOptions": { "strict": true }, "test": { "files": { "exclude": ["npm"] } }, "lint": { "files": { "exclude": ["npm"] } }, "fmt": { "files": { "exclude": ["npm"] }, "options": { "proseWrap": "preserve" } } }
-
-
-
@@ -1,17 +1,17 @@module.exports = { title: 'slack-message-parser', description: 'Documentation for slack-message-parser', title: "slack-message-parser", description: "Documentation for slack-message-parser", // Assume hosted on GitHub Pages. base: '/slack-message-parser/', base: "/slack-message-parser/", themeConfig: { nav: [ { text: 'Guide', link: '/' }, { text: 'API', link: '/api/' }, { text: "Guide", link: "/" }, { text: "API", link: "/api/" }, { text: 'GitHub', link: 'https://github.com/pocka/slack-message-parser' } text: "GitHub", link: "https://github.com/pocka/slack-message-parser", }, ], sidebar: ['/', '/api/'] } } sidebar: ["/", "/api/"], }, };
-
-
-
@@ -17,11 +17,11 @@Just parse the message! ```ts import slackMessageParser from 'slack-message-parser' import slackMessageParser from "slack-message-parser"; const tree = slackMessageParser('Slack *message* ~to~ _parse_') const tree = slackMessageParser("Slack *message* ~to~ _parse_"); console.dir(tree) console.dir(tree); ``` ## Supported Message Features
-
-
-
@@ -32,13 +32,13 @@You can test the type with `NodeType` object (which is actually TypeScript enum). ```js import { NodeType } from 'slack-message-parser' import { NodeType } from "slack-message-parser"; switch (node.type) { case NodeType.Text: // ... case NodeType.ChannelLink: // ... // ... } ```
-
-
mod.ts (new)
-
@@ -0,0 +1,58 @@import { Node, NodeType, Root } from "./types/Node.ts"; import { ParseText } from "./types/Parser.ts"; import parser from "./parser.ts"; const parseText: ParseText = (text) => { const children: Node[] = []; let textBuffer = ""; const flush = () => { if (!textBuffer) { return; } children.push({ type: NodeType.Text, text: textBuffer, source: textBuffer, }); textBuffer = ""; }; let i = 0; const l = text.length; while (i < l) { const match = parser(text, i, parseText); if (match) { flush(); const [node, position] = match; children.push(node); i = position; continue; } textBuffer += text.charAt(i); i += 1; } flush(); return children; }; export const parse = (message: string): Root => { return { type: NodeType.Root, children: parseText(message), source: message, }; }; export default parse; export * from "./types/Node.ts";
-
-
mod_test.ts (new)
-
@@ -0,0 +1,292 @@import { assertEquals } from "https://deno.land/std@0.161.0/testing/asserts.ts"; import { parse } from "./mod.ts"; import { bold, channel, code, command, emoji, italic, pre, quote, root, strike, text, url, user, } from "./tests/helpers.ts"; Deno.test(`Should parse "foo bar" as text`, () => { assertEquals(parse("foo bar"), root([text("foo bar")])); }); Deno.test('Should parse "`foo`" as code', () => { assertEquals( parse("`foo`"), root([code("foo")]), ); }); Deno.test('Should parse "` foo `" as code', () => { assertEquals( parse("` foo `"), root([code(" foo ")]), ); }); Deno.test('Should parse "```foo```" as pretext', () => { assertEquals( parse("```foo```"), root([pre("foo")]), ); }); Deno.test('Should parse "``` foo ```" as pretext', () => { assertEquals( parse("``` foo ```"), root([pre(" foo ")]), ); }); Deno.test('Should parse "``` foo ``` bar ``` baz ```" as two pretexts', () => { assertEquals( parse("``` foo ``` bar ``` baz ```"), root([pre(" foo "), text(" bar "), pre(" baz ")]), ); }); Deno.test('Should not parse "``` ```" as pretext', () => { assertEquals(parse("``` ```"), root([text("``` ```")])); }); Deno.test('Should not parse "foo```bar```baz" as pretext', () => { assertEquals(parse("foo```bar```baz"), root([text("foo```bar```baz")])); }); Deno.test('Should parse "*foo*" as bold', () => { assertEquals(parse("*foo*"), root([bold([text("foo")])])); }); Deno.test('Should parse "foo *bar* baz" as bold', () => { assertEquals( parse("foo *bar* baz"), root([text("foo "), bold([text("bar")]), text(" baz")]), ); }); Deno.test('Should not parse "foo*bar*baz" as bold', () => { assertEquals(parse("foo*bar*baz"), root([text("foo*bar*baz")])); }); Deno.test('Should not parse "*foo*bar" as bold', () => { assertEquals(parse("*foo*bar"), root([text("*foo*bar")])); }); Deno.test('Should not parse "foo * bar * baz" as bold', () => { assertEquals(parse("foo * bar * baz"), root([text("foo * bar * baz")])); }); Deno.test('Should parse "Hey, <@FOO>!" as user link', () => { assertEquals( parse("Hey, <@FOO>!"), root([text("Hey, "), user("FOO"), text("!")]), ); }); Deno.test('Should parse "Goto <#FOO>?" as channel link', () => { assertEquals( parse("Goto <#FOO>?"), root([text("Goto "), channel("FOO"), text("?")]), ); }); Deno.test('Should parse "foo <!bar> baz" as command', () => { assertEquals( parse("foo <!bar> baz"), root([text("foo "), command("bar", []), text(" baz")]), ); }); Deno.test("Should parse command arguments", () => { assertEquals(parse("<!foo^bar>"), root([command("foo", ["bar"])])); }); Deno.test('Should parse "Visit <http://foo.bar> or email to <mailto:foo@bar>" as url', () => { const expected = root([ text("Visit "), url("http://foo.bar"), text(" or email to "), url("mailto:foo@bar"), ]); assertEquals( parse("Visit <http://foo.bar> or email to <mailto:foo@bar>"), expected, ); }); Deno.test("Should not allow nested link", () => { assertEquals( parse("<http://foo|<http://bar>>"), root([text("<http://foo|"), url("http://bar"), text(">")]), ); }); Deno.test("Should parse label", () => { assertEquals( parse("<http://foo|bar>"), root([url("http://foo", [text("bar")])]), ); }); Deno.test("Should allow formated text in label", () => { const expected = root([ url("http://foo", [bold([text("bar "), strike([text("baz")])])]), ]); assertEquals(parse("<http://foo|*bar ~baz~*>"), expected); }); Deno.test('Should parse "foo :bar: baz" as emoji', () => { assertEquals( parse("foo :bar: baz"), root([text("foo "), emoji("bar"), text(" baz")]), ); }); Deno.test("Should parse emoji with skin-tone variation", () => { assertEquals( parse(":foo::skin-tone-1:"), root([emoji("foo", "skin-tone-1")]), ); }); Deno.test("Should parse sequential emojis", () => { assertEquals( parse("ab:cd::ef::skin-tone-1:g:h::i:jk"), root([ text("ab"), emoji("cd"), emoji("ef", "skin-tone-1"), text("g"), emoji("h"), emoji("i"), text("jk"), ]), ); }); Deno.test('Should parse "foo:bar:baz" as emoji', () => { assertEquals( parse("foo:bar:baz"), root([text("foo"), emoji("bar"), text("baz")]), ); }); Deno.test("Should not parse invalid emoji names", () => { assertEquals( parse("(11/3 - 4:30pm): ok"), root([text("(11/3 - 4:30pm): ok")]), ); }); Deno.test("Should parse quote text", () => { assertEquals( parse("> foo *bar*"), root([quote([text(" foo "), bold([text("bar")])], true)]), ); }); Deno.test("Should parse quote located in second line", () => { assertEquals( parse("foo\n>bar"), root([text("foo\n"), quote([text("bar")], true)]), ); }); Deno.test("Should parse multiline quote", () => { assertEquals( parse("foo\n>>>bar\n\nbaz"), root([text("foo\n"), quote([text("bar\n\nbaz")])]), ); }); Deno.test('Should not parse "foo>bar" as quote', () => { assertEquals(parse("foo>bar"), root([text("foo>bar")])); }); Deno.test('Should parse ">>>" as quoted ">>"', () => { assertEquals(parse(">>>"), root([quote([text(">>")], true)])); }); Deno.test('Should delimit with "?"', () => { assertEquals(parse("*foo*?"), root([bold([text("foo")]), text("?")])); }); Deno.test('Should delimit with "!"', () => { assertEquals(parse("*foo*!"), root([bold([text("foo")]), text("!")])); }); Deno.test('Should delimit with "."', () => { assertEquals(parse("*foo*."), root([bold([text("foo")]), text(".")])); }); Deno.test('Should delimit with "()"', () => { assertEquals( parse("(*foo*)"), root([text("("), bold([text("foo")]), text(")")]), ); assertEquals(parse(")*foo*("), root([text(")*foo*(")])); }); Deno.test('Should delimit with "[]"', () => { assertEquals( parse("[*foo*]"), root([text("["), bold([text("foo")]), text("]")]), ); assertEquals(parse("]*foo*["), root([text("]*foo*[")])); }); Deno.test('Should delimit with "{}"', () => { assertEquals( parse("{*foo*}"), root([text("{"), bold([text("foo")]), text("}")]), ); assertEquals(parse("}*foo*{"), root([text("}*foo*{")])); }); Deno.test('Should delimit with "-" or "="', () => { assertEquals( parse("-*foo*="), root([text("-"), bold([text("foo")]), text("=")]), ); }); Deno.test("Should parse slack message", () => { const expected = root([ user("FOO", [ text("abc "), strike([ text("def "), italic([ text("ghi "), bold([text("jkl "), emoji("+1")]), text(" mno"), ]), text(" pqr"), ]), text(" stu "), pre("vwx"), ]), ]); assertEquals( parse("<@FOO|abc ~def _ghi *jkl :+1:* mno_ pqr~ stu ```vwx```>"), expected, ); });
-
-
parser.ts (new)
-
@@ -0,0 +1,216 @@import { NodeType } from "./types/Node.ts"; import { explicit, or, regexp, topOfLine } from "./combinator.ts"; const parseBold = explicit( regexp( /^\*(\S([^*\n]*?|[^*\n]*? `.*?` )[^\s*]|\S)\*(?=[\s~!#$%^)\-+={}[\];:'",.?/]|$)/, (match, _text, position, parseText) => { const [matchedText, content] = match; return [ { type: NodeType.Bold, children: parseText(content), source: matchedText, }, position + matchedText.length, ]; }, ), ); const parseCode = explicit( regexp(/^`([^`]+?)`(?=[\s.,\])}!?\-=]|$)/, (match, _text, position) => { const [matchedText, content] = match; return [ { type: NodeType.Code, text: content, source: matchedText, }, position + matchedText.length, ]; }), ); const parsePreText = explicit( regexp( /^```(\s*\S[\s\S]*?\s*)```(?=[\s.,\])}!?\-=]|$)/, (match, _text, position) => { const [matchedText, content] = match; return [ { type: NodeType.PreText, text: content, source: matchedText, }, position + matchedText.length, ]; }, ), ); const parseItalic = explicit( regexp( /^_(\S([^_\n]*?|[^_\n]*? `.*?` )\S|\S)\_(?=[\s.,\])}!?\-=]|$)/, (match, _text, position, parseText) => { const [matchedText, content] = match; return [ { type: NodeType.Italic, children: parseText(content), source: matchedText, }, position + matchedText.length, ]; }, ), ); const parseStrike = explicit( regexp( /^~(\S([^~\n]*?|[^~\n]*? `.*?` )\S|\S)\~(?=[\s.,\])}!?\-=]|$)/, (match, _text, position, parseText) => { const [matchedText, content] = match; return [ { type: NodeType.Strike, children: parseText(content), source: matchedText, }, position + matchedText.length, ]; }, ), ); const parseSingleLineQuote = topOfLine( regexp(/^>(.*)(\n|$)/, (match, _text, position, parseText) => { const [matchedText, content] = match; const repeatedGt = content.match(/^((>)+)(.*)$/); return [ { type: NodeType.Quote, children: repeatedGt ? [ { type: NodeType.Text, text: repeatedGt[1], source: repeatedGt[1], }, ...parseText(repeatedGt[3]), ] : parseText(content), source: matchedText, }, position + matchedText.length, ]; }), ); const parseMultilineQuote = topOfLine( regexp(/^>>>([\s\S]+)$/, (match, _text, position, parseText) => { const [matchedText, content] = match; return [ { type: NodeType.Quote, children: parseText(content), source: matchedText, }, position + matchedText.length, ]; }), ); const parseEmoji = regexp( /^:([^:<`*#@!\s()$%]+):(:(skin-tone-.+?):)?/, (match, _text, position) => { const [matchedText, name, _, variation] = match; return [ { type: NodeType.Emoji, name, variation, source: matchedText, }, position + matchedText.length, ]; }, ); const parseLink = regexp( /^<([^\s<>][^\n<>]*?)(\|([^<>]+?))?>/, (match, _text, position, parseText) => { const [matchedText, link, _, label] = match; const nextPosition = position + matchedText.length; const labelNodes = label ? parseText(label) : undefined; switch (link.charAt(0)) { case "@": return [ { type: NodeType.UserLink, userID: link.slice(1), label: labelNodes, source: matchedText, }, nextPosition, ]; case "#": return [ { type: NodeType.ChannelLink, channelID: link.slice(1), label: labelNodes, source: matchedText, }, nextPosition, ]; case "!": { const [commandName, ...args] = link.slice(1).split("^"); return [ { type: NodeType.Command, name: commandName, arguments: args, label: labelNodes, source: matchedText, }, nextPosition, ]; } default: return [ { type: NodeType.URL, url: link, label: labelNodes, source: matchedText, }, nextPosition, ]; } }, ); export default or([ parseBold, parsePreText, parseCode, parseEmoji, parseItalic, parseMultilineQuote, parseSingleLineQuote, parseLink, parseStrike, ]);
-
-
scripts/build_npm.ts (new)
-
@@ -0,0 +1,38 @@import { build, emptyDir } from "https://deno.land/x/dnt@0.31.0/mod.ts"; await emptyDir("./npm"); await build({ entryPoints: ["./mod.ts"], outDir: "./npm", shims: { deno: true, }, package: { "name": "slack-message-parser", version: Deno.args[0], "description": "Parser for Slack message", "repository": { "type": "git", "url": "git+https://github.com/pocka/slack-message-parser.git", }, "keywords": [ "slack", ], "author": "pocka", "license": "MIT", "bugs": { "url": "https://github.com/pocka/slack-message-parser/issues", }, "homepage": "https://github.com/pocka/slack-message-parser#readme", "sideEffects": false, }, }); // Copy files Deno.copyFileSync("LICENSE", "npm/LICENSE"); Deno.copyFileSync("README.md", "npm/README.md"); Deno.mkdirSync("npm/docs"); Deno.mkdirSync("npm/docs/api"); Deno.copyFileSync("docs/README.md", "npm/docs/README.md"); Deno.copyFileSync("docs/api/README.md", "npm/docs/api/README.md");
-
-
src/combinator.ts (deleted)
-
@@ -1,62 +0,0 @@import { Node } from './types/Node' import { Parser, ParseText } from './types/Parser' export const or = (parsers: Parser[]): Parser => { const { length } = parsers return (text, position, rootParser) => { for (let i = 0; i < length; i++) { const match = parsers[i](text, position, rootParser) if (match) { return match } } return null } } export const regexp = ( pattern: RegExp, callback: ( match: string[], text: string, position: number, parseText: ParseText ) => [Node, number] | null ): Parser => (text, position, parseText) => { const match = text.substring(position).match(pattern) if (!match) { return null } return callback(match, text, position, parseText) } export const explicit = (parser: Parser): Parser => ( text, position, parseText ) => { const prevChar = text.charAt(position - 1) if (prevChar && !prevChar.match(/[\s.,([{!?\-=]/)) { return null } return parser(text, position, parseText) } export const topOfLine = (parser: Parser): Parser => ( text, position, parseText ) => { if (position > 0 && text.charAt(position - 1) !== '\n') { return null } return parser(text, position, parseText) }
-
-
src/index.ts (deleted)
-
@@ -1,58 +0,0 @@import { Node, NodeType, Root } from './types/Node' import { ParseText } from './types/Parser' import parser from './parser' const parseText: ParseText = text => { const children: Node[] = [] let textBuffer = '' const flush = () => { if (!textBuffer) { return } children.push({ type: NodeType.Text, text: textBuffer, source: textBuffer }) textBuffer = '' } let i = 0 const l = text.length while (i < l) { const match = parser(text, i, parseText) if (match) { flush() const [node, position] = match children.push(node) i = position continue } textBuffer += text.charAt(i) i += 1 } flush() return children } export const parse = (message: string): Root => { return { type: NodeType.Root, children: parseText(message), source: message } } export default parse export * from './types/Node'
-
-
src/parser.ts (deleted)
-
@@ -1,215 +0,0 @@import { NodeType } from './types/Node' import { explicit, or, regexp, topOfLine } from './combinator' const parseBold = explicit( regexp( /^\*(\S([^*\n]*?|[^*\n]*? `.*?` )[^\s*]|\S)\*(?=[\s~!#$%^)\-+={}[\];:'",.?/]|$)/, (match, text, position, parseText) => { const [matchedText, content] = match return [ { type: NodeType.Bold, children: parseText(content), source: matchedText }, position + matchedText.length ] } ) ) const parseCode = explicit( regexp(/^`([^`]+?)`(?=[\s.,\])}!?\-=]|$)/, (match, text, position) => { const [matchedText, content] = match return [ { type: NodeType.Code, text: content, source: matchedText }, position + matchedText.length ] }) ) const parsePreText = explicit( regexp( /^```(\s*\S[\s\S]*?\s*)```(?=[\s.,\])}!?\-=]|$)/, (match, text, position) => { const [matchedText, content] = match return [ { type: NodeType.PreText, text: content, source: matchedText }, position + matchedText.length ] } ) ) const parseItalic = explicit( regexp( /^_(\S([^_\n]*?|[^_\n]*? `.*?` )\S|\S)\_(?=[\s.,\])}!?\-=]|$)/, (match, text, position, parseText) => { const [matchedText, content] = match return [ { type: NodeType.Italic, children: parseText(content), source: matchedText }, position + matchedText.length ] } ) ) const parseStrike = explicit( regexp( /^~(\S([^~\n]*?|[^~\n]*? `.*?` )\S|\S)\~(?=[\s.,\])}!?\-=]|$)/, (match, text, position, parseText) => { const [matchedText, content] = match return [ { type: NodeType.Strike, children: parseText(content), source: matchedText }, position + matchedText.length ] } ) ) const parseSingleLineQuote = topOfLine( regexp(/^>(.*)(\n|$)/, (match, text, position, parseText) => { const [matchedText, content] = match const repeatedGt = content.match(/^((>)+)(.*)$/) return [ { type: NodeType.Quote, children: repeatedGt ? [ { type: NodeType.Text, text: repeatedGt[1], source: repeatedGt[1] }, ...parseText(repeatedGt[3]) ] : parseText(content), source: matchedText }, position + matchedText.length ] }) ) const parseMultilineQuote = topOfLine( regexp(/^>>>([\s\S]+)$/, (match, text, position, parseText) => { const [matchedText, content] = match return [ { type: NodeType.Quote, children: parseText(content), source: matchedText }, position + matchedText.length ] }) ) const parseEmoji = regexp( /^:([^:<`*#@!\s()$%]+):(:(skin-tone-.+?):)?/, (match, text, position) => { const [matchedText, name, _, variation] = match return [ { type: NodeType.Emoji, name, variation, source: matchedText }, position + matchedText.length ] } ) const parseLink = regexp( /^<([^\s<>][^\n<>]*?)(\|([^<>]+?))?>/, (match, text, position, parseText) => { const [matchedText, link, _, label] = match const nextPosition = position + matchedText.length const labelNodes = label ? parseText(label) : undefined switch (link.charAt(0)) { case '@': return [ { type: NodeType.UserLink, userID: link.slice(1), label: labelNodes, source: matchedText }, nextPosition ] case '#': return [ { type: NodeType.ChannelLink, channelID: link.slice(1), label: labelNodes, source: matchedText }, nextPosition ] case '!': const [commandName, ...args] = link.slice(1).split('^') return [ { type: NodeType.Command, name: commandName, arguments: args, label: labelNodes, source: matchedText }, nextPosition ] default: return [ { type: NodeType.URL, url: link, label: labelNodes, source: matchedText }, nextPosition ] } } ) export default or([ parseBold, parsePreText, parseCode, parseEmoji, parseItalic, parseMultilineQuote, parseSingleLineQuote, parseLink, parseStrike ])
-
-
src/types/Node.ts (deleted)
-
@@ -1,110 +0,0 @@export enum NodeType { Text, ChannelLink, UserLink, URL, Command, Emoji, PreText, Code, Italic, Bold, Strike, Quote, Root } export type Node = | Text | ChannelLink | UserLink | URL | Command | Emoji | PreText | Code | Italic | Bold | Strike | Quote | Root interface NodeBase { type: NodeType /** * Raw node text. */ source: string } export interface Text extends NodeBase { type: NodeType.Text text: string } export interface ChannelLink extends NodeBase { type: NodeType.ChannelLink channelID: string label?: Node[] } export interface UserLink extends NodeBase { type: NodeType.UserLink userID: string label?: Node[] } export interface URL extends NodeBase { type: NodeType.URL url: string label?: Node[] } export interface Command extends NodeBase { type: NodeType.Command name: string arguments: string[] label?: Node[] } export interface Emoji extends NodeBase { type: NodeType.Emoji name: string variation?: string } export interface PreText extends NodeBase { type: NodeType.PreText text: string } export interface Code extends NodeBase { type: NodeType.Code text: string } export interface Italic extends NodeBase { type: NodeType.Italic children: Node[] } export interface Bold extends NodeBase { type: NodeType.Bold children: Node[] } export interface Strike extends NodeBase { type: NodeType.Strike children: Node[] } export interface Quote extends NodeBase { type: NodeType.Quote children: Node[] } export interface Root extends NodeBase { type: NodeType.Root children: Node[] }
-
-
src/types/Parser.ts (deleted)
-
@@ -1,9 +0,0 @@import { Node } from './Node' export type Parser = ( text: string, position: number, parseText: ParseText ) => [Node, number] | null export type ParseText = (text: string) => Node[]
-
-
-
@@ -1,95 +1,95 @@import { Node, NodeType } from '../src/types/Node' import { Node, NodeType } from "../types/Node.ts"; function source(children: Node[]): string { return children.map(c => c.source).join('') return children.map((c) => c.source).join(""); } export const root = (children: Node[]): Node => ({ type: NodeType.Root, children, source: source(children) }) source: source(children), }); export const text = (t: string): Node => ({ type: NodeType.Text, text: t, source: t }) source: t, }); export const strike = (children: Node[]): Node => ({ type: NodeType.Strike, children, source: `~${source(children)}~` }) source: `~${source(children)}~`, }); export const italic = (children: Node[]): Node => ({ type: NodeType.Italic, children, source: `_${source(children)}_` }) source: `_${source(children)}_`, }); export const bold = (children: Node[]): Node => ({ type: NodeType.Bold, children, source: `*${source(children)}*` }) source: `*${source(children)}*`, }); export const code = (text: string): Node => ({ type: NodeType.Code, text, source: '`' + text + '`' }) source: "`" + text + "`", }); export const pre = (text: string): Node => ({ type: NodeType.PreText, text, source: '```' + text + '```' }) source: "```" + text + "```", }); export const user = (userID: string, label?: Node[]): Node => ({ type: NodeType.UserLink, userID, label, source: `<@${userID}${label ? '|' + source(label) : ''}>` }) source: `<@${userID}${label ? "|" + source(label) : ""}>`, }); export const channel = (channelID: string, label?: Node[]): Node => ({ type: NodeType.ChannelLink, channelID, label, source: `<#${channelID}${label ? '|' + source(label) : ''}>` }) source: `<#${channelID}${label ? "|" + source(label) : ""}>`, }); export const command = ( name: string, args: string[], label?: Node[] label?: Node[], ): Node => ({ type: NodeType.Command, name, arguments: args, label, source: `<!${name}${args.map(c => `^${c}`).join('')}${ label ? '|' + source(label) : '' }>` }) source: `<!${name}${args.map((c) => `^${c}`).join("")}${ label ? "|" + source(label) : "" }>`, }); export const url = (link: string, label?: Node[]): Node => ({ type: NodeType.URL, url: link, label, source: `<${link}${label ? '|' + source(label) : ''}>` }) source: `<${link}${label ? "|" + source(label) : ""}>`, }); export const emoji = (name: string, variation?: string): Node => ({ type: NodeType.Emoji, name, variation, source: `:${name}${variation ? '::' + variation : ''}:` }) source: `:${name}${variation ? "::" + variation : ""}:`, }); export const quote = (children: Node[], inline?: boolean): Node => ({ type: NodeType.Quote, children, source: `${'>'.repeat(inline ? 1 : 3)}${source(children)}` }) source: `${">".repeat(inline ? 1 : 3)}${source(children)}`, });
-
-
tests/index.spec.ts (deleted)
-
@@ -1,281 +0,0 @@import { parse } from '../src' import { bold, channel, code, command, emoji, italic, pre, quote, root, strike, text, url, user } from './helpers' describe('Text parser', () => { it('Should parse "foo bar" as text', () => { expect(parse('foo bar')).toEqual(root([text('foo bar')])) }) }) describe('Code parser', () => { it('Should parse "`foo`" as code', () => { expect(parse('`foo`')).toEqual(root([code('foo')])) }) it('Should parse "` foo `" as code', () => { expect(parse('` foo `')).toEqual(root([code(' foo ')])) }) }) describe('PreText parser', () => { it('Should parse "```foo```" as pretext', () => { expect(parse('```foo```')).toEqual(root([pre('foo')])) }) it('Should parse "``` foo ```" as pretext', () => { expect(parse('``` foo ```')).toEqual(root([pre(' foo ')])) }) it('Should parse "``` foo ``` bar ``` baz ```" as two pretexts', () => { expect(parse('``` foo ``` bar ``` baz ```')).toEqual( root([pre(' foo '), text(' bar '), pre(' baz ')]) ) }) it('Should not parse "``` ```" as pretext', () => { expect(parse('``` ```')).toEqual(root([text('``` ```')])) }) it('Should not parse "foo```bar```baz" as pretext', () => { expect(parse('foo```bar```baz')).toEqual(root([text('foo```bar```baz')])) }) }) describe('Bold parser', () => { it('Should parse "*foo*" as bold', () => { expect(parse('*foo*')).toEqual(root([bold([text('foo')])])) }) it('Should parse "foo *bar* baz" as bold', () => { expect(parse('foo *bar* baz')).toEqual( root([text('foo '), bold([text('bar')]), text(' baz')]) ) }) it('Should not parse "foo*bar*baz" as bold', () => { expect(parse('foo*bar*baz')).toEqual(root([text('foo*bar*baz')])) }) it('Should not parse "*foo*bar" as bold', () => { expect(parse('*foo*bar')).toEqual(root([text('*foo*bar')])) }) it('Should not parse "foo * bar * baz" as bold', () => { expect(parse('foo * bar * baz')).toEqual(root([text('foo * bar * baz')])) }) }) describe('Link parser', () => { it('Should parse "Hey, <@FOO>!" as user link', () => { expect(parse('Hey, <@FOO>!')).toEqual( root([text('Hey, '), user('FOO'), text('!')]) ) }) it('Should parse "Goto <#FOO>?" as channel link', () => { expect(parse('Goto <#FOO>?')).toEqual( root([text('Goto '), channel('FOO'), text('?')]) ) }) it('Should parse "foo <!bar> baz" as command', () => { expect(parse('foo <!bar> baz')).toEqual( root([text('foo '), command('bar', []), text(' baz')]) ) }) it('Should parse command arguments', () => { expect(parse('<!foo^bar>')).toEqual(root([command('foo', ['bar'])])) }) it('Should parse "Visit <http://foo.bar> or email to <mailto:foo@bar>" as url', () => { const expected = root([ text('Visit '), url('http://foo.bar'), text(' or email to '), url('mailto:foo@bar') ]) expect( parse('Visit <http://foo.bar> or email to <mailto:foo@bar>') ).toEqual(expected) }) // https://github.com/pocka/slack-message-parser/issues/1 it('Should parse url contains underscore', () => { expect(parse('<http://foo/bar_baz>')).toEqual( root([url('http://foo/bar_baz')]) ) }) it('Should not allow nested link', () => { expect(parse('<http://foo|<http://bar>>')).toEqual( root([text('<http://foo|'), url('http://bar'), text('>')]) ) }) it('Should parse label', () => { expect(parse('<http://foo|bar>')).toEqual( root([url('http://foo', [text('bar')])]) ) }) it('Should allow formated text in label', () => { const expected = root([ url('http://foo', [bold([text('bar '), strike([text('baz')])])]) ]) expect(parse('<http://foo|*bar ~baz~*>')).toEqual(expected) }) }) describe('Emoji parser', () => { it('Should parse "foo :bar: baz" as emoji', () => { expect(parse('foo :bar: baz')).toEqual( root([text('foo '), emoji('bar'), text(' baz')]) ) }) it('Should parse emoji with skin-tone variation', () => { expect(parse(':foo::skin-tone-1:')).toEqual( root([emoji('foo', 'skin-tone-1')]) ) }) it('Should parse sequential emojis', () => { expect(parse('ab:cd::ef::skin-tone-1:g:h::i:jk')).toEqual( root([ text('ab'), emoji('cd'), emoji('ef', 'skin-tone-1'), text('g'), emoji('h'), emoji('i'), text('jk') ]) ) }) it('Should parse "foo:bar:baz" as emoji', () => { expect(parse('foo:bar:baz')).toEqual( root([text('foo'), emoji('bar'), text('baz')]) ) }) it('Should not parse invalid emoji names', () => { expect(parse('(11/3 - 4:30pm): ok')).toEqual( root([text('(11/3 - 4:30pm): ok')]) ) }) }) describe('Quote parser', () => { it('Should parse quote text', () => { expect(parse('> foo *bar*')).toEqual( root([quote([text(' foo '), bold([text('bar')])], true)]) ) }) it('Should parse quote locate in second line', () => { expect(parse('foo\n>bar')).toEqual( root([text('foo\n'), quote([text('bar')], true)]) ) }) it('Should parse multiline quote', () => { expect(parse('foo\n>>>bar\n\nbaz')).toEqual( root([text('foo\n'), quote([text('bar\n\nbaz')])]) ) }) it('Should not parse "foo>bar" as quote', () => { expect(parse('foo>bar')).toEqual(root([text('foo>bar')])) }) it('Should parse ">>>" as quoted ">>"', () => { expect(parse('>>>')).toEqual( root([quote([text('>>')], true)]) ) }) }) describe('Punctuations', () => { it('Should delimit with "?"', () => { expect(parse('*foo*?')).toEqual(root([bold([text('foo')]), text('?')])) }) it('Should delimit with "!"', () => { expect(parse('*foo*!')).toEqual(root([bold([text('foo')]), text('!')])) }) it('Should delimit with "."', () => { expect(parse('*foo*.')).toEqual(root([bold([text('foo')]), text('.')])) }) it('Should delimit with "()"', () => { expect(parse('(*foo*)')).toEqual( root([text('('), bold([text('foo')]), text(')')]) ) expect(parse(')*foo*(')).toEqual(root([text(')*foo*(')])) }) it('Should delimit with "[]"', () => { expect(parse('[*foo*]')).toEqual( root([text('['), bold([text('foo')]), text(']')]) ) expect(parse(']*foo*[')).toEqual(root([text(']*foo*[')])) }) it('Should delimit with "{}"', () => { expect(parse('{*foo*}')).toEqual( root([text('{'), bold([text('foo')]), text('}')]) ) expect(parse('}*foo*{')).toEqual(root([text('}*foo*{')])) }) it('Should delimit with "-" or "="', () => { expect(parse('-*foo*=')).toEqual( root([text('-'), bold([text('foo')]), text('=')]) ) }) }) describe('Root parser', () => { it('Should parse slack message', () => { const expected = root([ user('FOO', [ text('abc '), strike([ text('def '), italic([ text('ghi '), bold([text('jkl '), emoji('+1')]), text(' mno') ]), text(' pqr') ]), text(' stu '), pre('vwx') ]) ]) expect( parse('<@FOO|abc ~def _ghi *jkl :+1:* mno_ pqr~ stu ```vwx```>') ).toEqual(expected) }) })
-
-
tests/issues.spec.ts (deleted)
-
@@ -1,120 +0,0 @@import { parse } from '../src' import { bold, code, emoji, italic, url, root, strike, text } from './helpers' describe('#4', () => { it('Should parse correctly', () => { const test = 'This is _the_ *first* ~program~ `code of the rest of this` *file*, err, _file_, and by ~file~, I mean *file*.' const expected = root([ text('This is '), italic([text('the')]), text(' '), bold([text('first')]), text(' '), strike([text('program')]), text(' '), code('code of the rest of this'), text(' '), bold([text('file')]), text(', err, '), italic([text('file')]), text(', and by '), strike([text('file')]), text(', I mean '), bold([text('file')]), text('.') ]) expect(parse(test)).toEqual(expected) }) }) describe('#6', () => { it('Treat only "skin-tone-*" as variations', () => { expect(parse(':a::b:')).toEqual(root([emoji('a'), emoji('b')])) }) }) // https://github.com/pocka/slack-message-parser/issues/13 describe('#13', () => { // According to RFC 2368, spaces in mailto URL should be encoded // but Slack accpets unencoded spaces. it('Parse mailto link contains spaces', () => { expect( parse('<mailto:foo@bar.baz?subject=Hello, World&body=https://foo.bar>') ).toEqual( root([ url('mailto:foo@bar.baz?subject=Hello, World&body=https://foo.bar') ]) ) }) }) describe('#22', () => { it('doesnt match colons and new lines as emojis', () => { expect(parse('Test:\nTest 2:\nTest 3:')).toEqual( root([text('Test:\nTest 2:\nTest 3:')]) ) }) }) // https://github.com/pocka/slack-message-parser/issues/34 describe('#34', () => { it('parses bold formatting properly with various punctuation suffixes', () => { expect( parse( '*Y*~ *N*` *Y*! *N*@ *Y*# *Y*$ *Y*% *Y*^ *N*& *N** *N*( *Y*) *N*_ *Y*- *Y*+ *Y*= *Y*{ *Y*} *Y*[ *Y*] *N*| *N*\\ *Y*; *Y*: *Y*\' *Y*" *N*< *Y*, *N*> *Y*. *Y*? *Y*/ *Y*' ) ).toEqual( root([ bold([text('Y')]), text('~ *N*` '), bold([text('Y')]), text('! *N*@ '), bold([text('Y')]), text('# '), bold([text('Y')]), text('$ '), bold([text('Y')]), text('% '), bold([text('Y')]), text('^ *N*& *N** *N*( '), bold([text('Y')]), text(') *N*_ '), bold([text('Y')]), text('- '), bold([text('Y')]), text('+ '), bold([text('Y')]), text('= '), bold([text('Y')]), text('{ '), bold([text('Y')]), text('} '), bold([text('Y')]), text('[ '), bold([text('Y')]), text('] *N*| *N*\\ '), bold([text('Y')]), text('; '), bold([text('Y')]), text(': '), bold([text('Y')]), text("' "), bold([text('Y')]), text('" *N*< '), bold([text('Y')]), text(', *N*> '), bold([text('Y')]), text('. '), bold([text('Y')]), text('? '), bold([text('Y')]), text('/ '), bold([text('Y')]) ]) ) }) })
-
-
tests/issues.test.ts (new)
-
@@ -0,0 +1,131 @@import { assertEquals } from "https://deno.land/std@0.161.0/testing/asserts.ts"; import { parse } from "../mod.ts"; import { bold, code, emoji, italic, root, strike, text, url, } from "./helpers.ts"; // https://github.com/pocka/slack-message-parser/issues/1 Deno.test(`#1 / Should parse url contains underscore`, () => { assertEquals( parse("<http://foo/bar_baz>"), root([url("http://foo/bar_baz")]), ); }); // https://github.com/pocka/slack-message-parser/issues/4 Deno.test(`#4 / Should parse correctly`, () => { const test = "This is _the_ *first* ~program~ `code of the rest of this` *file*, err, _file_, and by ~file~, I mean *file*."; const expected = root([ text("This is "), italic([text("the")]), text(" "), bold([text("first")]), text(" "), strike([text("program")]), text(" "), code("code of the rest of this"), text(" "), bold([text("file")]), text(", err, "), italic([text("file")]), text(", and by "), strike([text("file")]), text(", I mean "), bold([text("file")]), text("."), ]); assertEquals(parse(test), expected); }); // https://github.com/pocka/slack-message-parser/issues/6 Deno.test(`#6 / Treat only "skin-tone-*" as variations`, () => { assertEquals(parse(":a::b:"), root([emoji("a"), emoji("b")])); }); // https://github.com/pocka/slack-message-parser/issues/13 // According to RFC 2368, spaces in mailto URL should be encoded // but Slack accpets unencoded spaces. Deno.test(`#13 / Parse mailto link contains spaces`, () => { assertEquals( parse("<mailto:foo@bar.baz?subject=Hello, World&body=https://foo.bar>"), root([ url("mailto:foo@bar.baz?subject=Hello, World&body=https://foo.bar"), ]), ); }); // https://github.com/pocka/slack-message-parser/issues/22 Deno.test(`#22 / doesnt match colons and new lines as emojis`, () => { assertEquals( parse("Test:\nTest 2:\nTest 3:"), root([text("Test:\nTest 2:\nTest 3:")]), ); }); // https://github.com/pocka/slack-message-parser/issues/34 Deno.test(`#34 / parses bold formatting properly with various punctuation suffixes`, () => { assertEquals( parse( "*Y*~ *N*` *Y*! *N*@ *Y*# *Y*$ *Y*% *Y*^ *N*& *N** *N*( *Y*) *N*_ *Y*- *Y*+ *Y*= *Y*{ *Y*} *Y*[ *Y*] *N*| *N*\\ *Y*; *Y*: *Y*' *Y*\" *N*< *Y*, *N*> *Y*. *Y*? *Y*/ *Y*", ), root([ bold([text("Y")]), text("~ *N*` "), bold([text("Y")]), text("! *N*@ "), bold([text("Y")]), text("# "), bold([text("Y")]), text("$ "), bold([text("Y")]), text("% "), bold([text("Y")]), text("^ *N*& *N** *N*( "), bold([text("Y")]), text(") *N*_ "), bold([text("Y")]), text("- "), bold([text("Y")]), text("+ "), bold([text("Y")]), text("= "), bold([text("Y")]), text("{ "), bold([text("Y")]), text("} "), bold([text("Y")]), text("[ "), bold([text("Y")]), text("] *N*| *N*\\ "), bold([text("Y")]), text("; "), bold([text("Y")]), text(": "), bold([text("Y")]), text("' "), bold([text("Y")]), text('" *N*< '), bold([text("Y")]), text(", *N*> "), bold([text("Y")]), text(". "), bold([text("Y")]), text("? "), bold([text("Y")]), text("/ "), bold([text("Y")]), ]), ); });
-
-
tests/tsconfig.json (deleted)
-
@@ -1,4 +0,0 @@{ "extends": "../tsconfig.json", "include": ["../src/**/*", "./**/*"] }
-
-
tsconfig.json (deleted)
-
@@ -1,15 +0,0 @@{ "compilerOptions": { "module": "commonjs", "strict": true, "noImplicitAny": true, "removeComments": true, "preserveConstEnums": true, "sourceMap": true, "target": "es2015", "outDir": "lib", "declaration": true }, "include": ["src/**/*"], "exclude": ["node_modules"] }
-
-
tslint.json (deleted)
-
@@ -1,14 +0,0 @@{ "defaultSeverity": "error", "extends": [ "tslint:recommended", "tslint-config-prettier" ], "jsRules": {}, "rules": { "no-console": false, "interface-name": false, "object-literal-sort-keys": false }, "rulesDirectory": [] }
-
-
types/Node.ts (new)
-
@@ -0,0 +1,110 @@export enum NodeType { Text, ChannelLink, UserLink, URL, Command, Emoji, PreText, Code, Italic, Bold, Strike, Quote, Root, } export type Node = | Text | ChannelLink | UserLink | URL | Command | Emoji | PreText | Code | Italic | Bold | Strike | Quote | Root; interface NodeBase { type: NodeType; /** * Raw node text. */ source: string; } export interface Text extends NodeBase { type: NodeType.Text; text: string; } export interface ChannelLink extends NodeBase { type: NodeType.ChannelLink; channelID: string; label?: Node[]; } export interface UserLink extends NodeBase { type: NodeType.UserLink; userID: string; label?: Node[]; } export interface URL extends NodeBase { type: NodeType.URL; url: string; label?: Node[]; } export interface Command extends NodeBase { type: NodeType.Command; name: string; arguments: string[]; label?: Node[]; } export interface Emoji extends NodeBase { type: NodeType.Emoji; name: string; variation?: string; } export interface PreText extends NodeBase { type: NodeType.PreText; text: string; } export interface Code extends NodeBase { type: NodeType.Code; text: string; } export interface Italic extends NodeBase { type: NodeType.Italic; children: Node[]; } export interface Bold extends NodeBase { type: NodeType.Bold; children: Node[]; } export interface Strike extends NodeBase { type: NodeType.Strike; children: Node[]; } export interface Quote extends NodeBase { type: NodeType.Quote; children: Node[]; } export interface Root extends NodeBase { type: NodeType.Root; children: Node[]; }
-
-
types/Parser.ts (new)
-
@@ -0,0 +1,9 @@import { Node } from "./Node.ts"; export type Parser = ( text: string, position: number, parseText: ParseText, ) => [Node, number] | null; export type ParseText = (text: string) => Node[];
-