Compare commits

...

10 Commits

Author SHA1 Message Date
Sol Fisher Romanoff 982958ce2b
Mention dingus in README
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-08-01 12:40:07 +03:00
Sol Fisher Romanoff 2dcb6d82be
Add backslash escaping
ci/woodpecker/tag/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-08-01 09:08:18 +03:00
Sol Fisher Romanoff d6e115fbb8
Add non-rawhtml center tag
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-07-31 21:51:29 +03:00
Sol Fisher Romanoff cb12e3a149
Add dingus
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-07-31 21:36:37 +03:00
Sol Fisher Romanoff 600dd77e50
Add syntax document
ci/woodpecker/pr/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-07-15 14:59:00 +03:00
Sol Fisher Romanoff 092b141f5b
Update README with contributing info
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-07-13 21:03:53 +03:00
Sol Fisher Romanoff 4d885754fd
Remove search
ci/woodpecker/tag/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
fuck search
2022-07-12 20:29:43 +03:00
Sol Fisher Romanoff c748bacc0f
Exit regex matching on unclosed MFM tag
ci/woodpecker/tag/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-07-10 22:54:58 +03:00
Sol Fisher Romanoff 08c65455e5
Change search regex to use <br> instead of \n
ci/woodpecker/tag/woodpecker Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-07-10 18:58:50 +03:00
Sol Fisher Romanoff 4a6bd142d4
Fix tests
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-07-09 15:53:57 +03:00
8 changed files with 169 additions and 21 deletions

View File

@ -2,6 +2,7 @@
a [Marked.js](https://marked.js.org) custom extension for [Misskey-flavored Markdown](https://github.com/misskey-dev/mfm.js/blob/develop/docs/syntax.md).
this is a reimplementation of [the original MFM parser](https://github.com/misskey-dev/mfm.js/blob/develop/docs/syntax.md), which was written from the ground up using PEG.js instead of as an extension of CommonMark, and therefore was not up to spec.
this extension is mostly compatible with the original, except it only implements `$[tags]`. mentions, hashtags and the `search` markup are out of scope.
## usage
@ -11,5 +12,21 @@ const markedMfm = require('marked-mfm')
marked.use(markedMfm)
marked.parse('$[x2 beeg text]')
// <p><span class="_mfm_x2_" >beeg text</span></p>
// <p><span class="mfm _mfm_x2_" >beeg text</span></p>
```
there is also an interactive CLI:
```html
$ npm run dingus
marked-mfm interactive parser
version 0.0.0
> $[x2 beeg text]
<p><span class="mfm _mfm_x2_" >beeg text</span></p>
```
you should use it to figure out whether a bug is with marked-mfm or with the frontend it's used on.
## contributing
development discussion takes place on [`#mfm`](https://irc.akkoma.dev/#mfm). contribute by sending pull requests, or patches to my [public inbox](mailto:~sfr/public-inbox@lists.sr.ht).

69
docs/syntax.md Normal file
View File

@ -0,0 +1,69 @@
# Misskey-flavored Markdown syntax and cheat sheet
this document acts as the canonical source of information about MFM implemented
by `marked-mfm`. if you are looking for the implementation used in Misskey, refer
to [this page on
`mfm.js`](https://github.com/misskey-dev/mfm.js/blob/develop/docs/syntax.md).
## Markdown-to-HTML parser
all MFM elements are parsed to HTML like so:
```html
<span class="mfm _mfm_tag_" data-param1 data-param2>text</span>
```
refer to the rest of the document for information on tags and parameters.
## MFM tags
| MFM | summary |
|------------------|--------------------------------------------|
| `$[tada text]` | "tada!"-like animation |
| `$[jelly text]` | squashing and stretching animation |
| `$[twitch text]` | stuttering animation |
| `$[shake text]` | stuttering and rotating animation |
| `$[spin text]` | spinning animation |
| `$[jump text]` | jumping animation |
| `$[bounce text]` | jumping and squashing animation |
| `$[flip text]` | mirrors the element |
| `$[x2 text]` | big text |
| `$[x3 text]` | bigger text |
| `$[x4 text]` | even bigger text |
| `$[font text]` | sets a font do display the content in |
| `$[rotate text]` | rotates the element by a number of degrees |
## optional parameters
some MFM elements allow specifying additional attributes in the tag. this is
done by adding a `.` to the tag name, followed by a comma-separated list of
attributes.
example: `$[spin.alternate,speed=3s text]`
- **`speed`**
*valid in: `jelly`, `twitch`, `shake`, `spin`*
a number, in seconds, that defines the duration of the animation
example: `$[jelly.speed=2s text]`
- **`h`, `v`**
*valid in: `flip`*
defines the axis the element is flipped upon; horizontally, vertically, or
both
example: `$[flip.h.v text]`
- **`x`, `y`**
*valid in: `spin`*
defines the axis on which to spin the element
example: `$[spin.x text]`
- **`alternate`, `left`**
*valid in: `spin`*
defines whether to run the spinning animation in reverse, or to alternate it
example: `$[spin.left text]`
- **`serif`, `monospace`, `cursive`, `fantasy`, `emoji`, `math`**
*valid in: `font`*
sets the font inside the font element; if unset, the element does nothing
example: `$[font.fantasy text]`
* **`deg`**
*valid in: `rotate`*
a number of degrees to rotate the element; if unset, 30° is used
example: `$[rotate.deg=120 text]`

View File

@ -94,6 +94,10 @@
var walk = 0;
while (level > 0 || walk === 0) {
if (walk >= src.length) {
return null;
}
if (src[walk] + src[walk + 1] === '$[') {
level++;
walk++;
@ -144,22 +148,37 @@
return "$[" + token.tag + " " + this.parser.parseInline(token.tokens) + "]";
}
}, {
name: 'mfmSearch',
name: 'escapedMfm',
level: 'inline',
start: function start(src) {
var _src$match2;
return (_src$match2 = src.match(/(?<=^|\n).+ search(?=$|\n)/)) == null ? void 0 : _src$match2.index;
return src.match(/\\\$\[/);
},
tokenizer: function tokenizer(src, tokens) {
var rule = /^(.+) search(?=$|\n)/;
if (/^\\\$\[/.exec(src)) {
return {
type: 'escapedMfm',
raw: '\\$['
};
}
},
renderer: function renderer(token) {
return '$[';
}
}, {
name: 'center',
level: 'block',
start: function start(src) {
return src.match(/<center>/);
},
tokenizer: function tokenizer(src, tokens) {
var rule = /^<center>([\S\s]*)<\/center>/;
var match = rule.exec(src);
if (match) {
var token = {
type: 'mfmSearch',
type: 'center',
raw: match[0],
query: match[1].trim(),
text: match[1],
tokens: []
};
this.lexer.inline(token.text, token.tokens);
@ -167,7 +186,7 @@
}
},
renderer: function renderer(token) {
return "<a href=\"https://google.com/search?q=" + token.query + "\" class=\"mfm _mfm_search_\">" + token.raw + "</a>";
return "<center>" + this.parser.parseInline(token.tokens) + "</center>\n";
}
}]
};

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "marked-mfm",
"version": "0.1.0",
"version": "0.3.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "marked-mfm",
"version": "0.1.0",
"version": "0.3.1",
"license": "BlueOak-1.0.0",
"dependencies": {
"@babel/core": "^7.18.6",

View File

@ -1,6 +1,6 @@
{
"name": "marked-mfm",
"version": "0.2.1",
"version": "0.5.0",
"description": "Marked.js extension for Misskey-flavored Markdown",
"main": "./src/index.js",
"browser": "./lib/index.umd.js",
@ -9,7 +9,8 @@
"lint": "eslint .",
"lint-fix": "eslint --fix .",
"test": "jest --verbose",
"build": "rollup -c rollup.config.umd.js"
"build": "rollup -c rollup.config.umd.js",
"dingus": "node ./src/dingus.js"
},
"repository": {
"type": "git",

21
src/dingus.js Normal file
View File

@ -0,0 +1,21 @@
import readline from 'readline'
import { marked } from 'marked'
import markedMfm from '../src/index.js'
const prompt = () => (new Promise((resolve) => {
const rl = readline.createInterface(process.stdin, process.stdout)
rl.question('> ', (input) => {
resolve(input)
rl.close()
})
}))
marked.use(markedMfm)
console.log('marked-mfm interactive parser')
console.log('version', process.env.npm_package_version)
while (true) {
const input = await prompt()
console.log(marked(input))
}

View File

@ -10,6 +10,9 @@ export default {
let level = 0
let walk = 0
while (level > 0 || walk === 0) {
if (walk >= src.length) {
return null
}
if (src[walk] + src[walk + 1] === '$[') {
level++
walk++
@ -48,17 +51,30 @@ export default {
},
},
{
name: 'mfmSearch',
name: 'escapedMfm',
level: 'inline',
start (src) { return src.match(/(?<=^|\n).+ search(?=$|\n)/)?.index },
start (src) { return src.match(/\\\$\[/) },
tokenizer (src, tokens) {
const rule = /^(.+) search(?=$|\n)/
if (/^\\\$\[/.exec(src)) {
return { type: 'escapedMfm', raw: '\\$[' }
}
},
renderer (token) {
return '$['
},
},
{
name: 'center',
level: 'block',
start (src) { return src.match(/<center>/) },
tokenizer (src, tokens) {
const rule = /^<center>([\S\s]*)<\/center>/
const match = rule.exec(src)
if (match) {
const token = {
type: 'mfmSearch',
type: 'center',
raw: match[0],
query: match[1].trim(),
text: match[1],
tokens: [],
}
this.lexer.inline(token.text, token.tokens)
@ -66,7 +82,7 @@ export default {
}
},
renderer (token) {
return `<a href="https://google.com/search?q=${token.query}" class="mfm _mfm_search_">${token.raw}</a>`
return `<center>${this.parser.parseInline(token.tokens)}</center>\n`
},
},
],

View File

@ -42,8 +42,13 @@ describe('marked-mfm', () => {
expect(marked('$[spin.alternate,speed=0.5s test]')).toBe('<p><span class="mfm _mfm_spin_" data-alternate data-speed="0.5s">test</span></p>\n')
})
test('search', () => {
test('escaped', () => {
marked.use(markedMfm)
expect(marked('syuilo thighs search')).toBe('<p><a href="https://google.com/search?q=syuilo thighs" class="_mfm_search_">syuilo thighs search</a></p>\n')
expect(marked('\\$[spin test]')).toBe('<p>$[spin test]</p>\n')
})
test('center', () => {
marked.use(markedMfm)
expect(marked('<center>test *italic*</center>')).toBe('<center>test <em>italic</em></center>\n')
})
})