|
| 1 | +import type MarkdownIt from "markdown-it"; |
| 2 | +import type ParserInline from "markdown-it/lib/parser_inline"; |
| 3 | +import type StateInline from "markdown-it/lib/rules_inline/state_inline"; |
| 4 | +import type Token from "markdown-it/lib/token"; |
| 5 | +import type { EditorPlugin } from "../../src"; |
| 6 | + |
| 7 | +// simple proof of concept that adds furigana support from https://japanese.meta.stackexchange.com/questions/806/how-should-i-format-my-questions-on-japanese-language-se/807#807 |
| 8 | +// due to the fact that we cannot directly alter the contenteditable content, we have to make these a node or mark |
| 9 | +// NOTE: functionality heavily inspired by https://cdn.sstatic.net/Js/third-party/japanese-l-u.js |
| 10 | +export const japaneseSEPlugin: EditorPlugin = () => ({ |
| 11 | + markdown: { |
| 12 | + parser: { |
| 13 | + jse_furigana: { |
| 14 | + mark: "jse_furigana", |
| 15 | + getAttrs: (token: Token) => { |
| 16 | + return { |
| 17 | + text: token.content, |
| 18 | + markup: token.attrGet("markup"), |
| 19 | + }; |
| 20 | + }, |
| 21 | + }, |
| 22 | + }, |
| 23 | + serializers: { |
| 24 | + nodes: {}, |
| 25 | + marks: { |
| 26 | + jse_furigana: { |
| 27 | + open: (_, mark) => mark.attrs.markup as string, |
| 28 | + close: (_, mark) => { |
| 29 | + const markup = mark.attrs.markup as string; |
| 30 | + return markup === "{" ? "}" : "】"; |
| 31 | + }, |
| 32 | + }, |
| 33 | + }, |
| 34 | + }, |
| 35 | + alterMarkdownIt: (mdit) => { |
| 36 | + mdit.use((md: MarkdownIt) => { |
| 37 | + md.inline.ruler.push("jse", mdJSEPlugin); |
| 38 | + }); |
| 39 | + }, |
| 40 | + }, |
| 41 | + extendSchema: (schema) => { |
| 42 | + schema.marks = schema.marks.addToEnd("jse_furigana", { |
| 43 | + attrs: { |
| 44 | + text: { default: "" }, |
| 45 | + markup: { default: "" }, |
| 46 | + }, |
| 47 | + toDOM: (mark) => { |
| 48 | + return [ |
| 49 | + "span", |
| 50 | + { |
| 51 | + "class": "jse-furigana", |
| 52 | + "data-text": mark.attrs.text as string, |
| 53 | + }, |
| 54 | + ]; |
| 55 | + }, |
| 56 | + parseDOM: [ |
| 57 | + { |
| 58 | + tag: "span.jse-furigana", |
| 59 | + }, |
| 60 | + { |
| 61 | + tag: "span.rt", |
| 62 | + }, |
| 63 | + ], |
| 64 | + }); |
| 65 | + |
| 66 | + return schema; |
| 67 | + }, |
| 68 | +}); |
| 69 | + |
| 70 | +function findEndChar( |
| 71 | + state: StateInline, |
| 72 | + start: number, |
| 73 | + disableNested: boolean, |
| 74 | + startCharCode: number, |
| 75 | + endCharCode: number |
| 76 | +) { |
| 77 | + let level, |
| 78 | + found, |
| 79 | + marker, |
| 80 | + prevPos, |
| 81 | + labelEnd = -1; |
| 82 | + const max = state.posMax, |
| 83 | + oldPos = state.pos; |
| 84 | + |
| 85 | + state.pos = start + 1; |
| 86 | + level = 1; |
| 87 | + |
| 88 | + while (state.pos < max) { |
| 89 | + marker = state.src.charCodeAt(state.pos); |
| 90 | + if (marker === endCharCode) { |
| 91 | + level--; |
| 92 | + if (level === 0) { |
| 93 | + found = true; |
| 94 | + break; |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + prevPos = state.pos; |
| 99 | + state.md.inline.skipToken(state); |
| 100 | + if (marker === startCharCode) { |
| 101 | + if (prevPos === state.pos - 1) { |
| 102 | + // increase level if we find text `startCharCode`, which is not a part of any token |
| 103 | + level++; |
| 104 | + } else if (disableNested) { |
| 105 | + state.pos = oldPos; |
| 106 | + return -1; |
| 107 | + } |
| 108 | + } |
| 109 | + } |
| 110 | + |
| 111 | + if (found) { |
| 112 | + labelEnd = state.pos; |
| 113 | + } |
| 114 | + |
| 115 | + // restore old state |
| 116 | + state.pos = oldPos; |
| 117 | + |
| 118 | + return labelEnd; |
| 119 | +} |
| 120 | + |
| 121 | +const mdJSEPlugin: ParserInline.RuleInline = function (state, silent) { |
| 122 | + const startCharCode = state.src.charCodeAt(state.pos); |
| 123 | + |
| 124 | + // quick fail on first character |
| 125 | + if (startCharCode !== 0x7b /* { */ && startCharCode !== 0x3010 /* 【 */) { |
| 126 | + return false; |
| 127 | + } |
| 128 | + |
| 129 | + const endCharCode = |
| 130 | + startCharCode === 0x7b /* { */ ? 0x7d /* } */ : 0x3011; /* 】 */ |
| 131 | + |
| 132 | + const endCharPos = findEndChar( |
| 133 | + state, |
| 134 | + state.pos + 1, |
| 135 | + false, |
| 136 | + startCharCode, |
| 137 | + endCharCode |
| 138 | + ); |
| 139 | + |
| 140 | + if (endCharPos < 0) { |
| 141 | + return false; |
| 142 | + } |
| 143 | + |
| 144 | + if (!silent) { |
| 145 | + const totalContent = state.src.slice(state.pos, endCharPos + 1); |
| 146 | + const text = totalContent.slice(1, -1); |
| 147 | + |
| 148 | + let token = state.push("jse_furigana_open", "span", 1); |
| 149 | + token.attrSet("markup", String.fromCharCode(startCharCode)); |
| 150 | + token.content = text; |
| 151 | + |
| 152 | + token = state.push("text", "", 0); |
| 153 | + token.content = text; |
| 154 | + |
| 155 | + token = state.push("jse_furigana_close", "span", -1); |
| 156 | + token.attrSet("markup", String.fromCharCode(endCharCode)); |
| 157 | + } |
| 158 | + |
| 159 | + state.pos = endCharPos + 1; |
| 160 | + return true; |
| 161 | +}; |
0 commit comments