bluesky client without react native baggage written in sveltekit
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add 12's composer

ansxor 2e28f8a4 4067f307

+2668 -9
+6
editor1/README.txt
··· 1 + 🚧 unfinished 🚧 2 + 3 + we are using esbuild (https://esbuild.github.io) for bundling 4 + start ./host.sh, then open http://[::1]:8067/test.html in a browser 5 + OR 6 + run ./build.sh, then open file:///.../test.html in a browser
+4
editor1/bundle.sh
··· 1 + ./node_modules/.bin/esbuild site/root.js --bundle --outdir=site/bundle --sourcemap --banner:js="'use strict'" --format=esm --target=firefox91,safari15,chrome999 "$@" 2 + # note, we set --format=esm even though we load as a normal script 3 + # this is to trick esbuild into defining all module-global variables as window globals 4 + # which is useful for testing (also note that esbuild avoids name collisions here)
+1
editor1/host.sh
··· 1 + ./bundle.sh --serve="[::1]:8067" --servedir='site' "$@"
+65
editor1/notes.txt
··· 1 + ok so with the parser we gotta output 2 things: 2 + 1: faceted text (i.e. text with a list of non-overlapping ranges with features) 3 + 2: highlighter spans (ie the exact text of the input, split into non-overlapping ranges with styling on them) 4 + 5 + for 2: 6 + 7 + there's basically 2 kinds of text we push. 8 + - syntax spans, which correspond to text which doesnt appear in the output (though may map to text in the output) 9 + - visible spans, which do appear in the output directly 10 + 11 + when the parser hits something that gets eaten and doesnt go to output (e.g. `**`) 12 + we need to: 13 + - flush the current regular text before it 14 + - create a new node in the tree, set that as the current node 15 + 16 + then, later on we have 2 options: 17 + - 'cancel' this node (ie move all its contents to the parent, set current node to parent) 18 + - 'close' this node (set current node to parent) 19 + 20 + ok so how about each highlight span has like, a copy of the array of currently open nodes, then at the end we filter out the ones that have like .cancelled=true, and convert it to a string? 21 + 22 + anyway so open(): 23 + what this actually does is say, 24 + - end current segment 25 + - add a feature to the list of features 26 + - set temp variable on that feature saying it is supporting that segment (segments have a reference count - well of course, thats the features array length) 27 + - start a new segment 28 + 29 + 30 + close(): 31 + - end current segment 32 + - remove feature from the list of features 33 + - start a new segment 34 + 35 + cancel(): 36 + - for each segment, if it contains that feature, remove it. we may need to merge the first segment containing this feature with the one before it, if their feature lists are identical now. 37 + - note, we can stay within the current segment 38 + - oh wait shit but we gotta go back and insert the syntax characters uhhh 39 + 40 + ok lets say that um 41 + we can only begin a single feature at a time. 42 + ie each segment has a feature that caused it to begin, and likewise each feature has a segment that it began. 43 + so lets say we have a situation like "ab /cd" 44 + we have segments: 45 + {text:"ab ",features:[]} 46 + {text:"cd", features:[{<italic>,oncancel:{prefix:"/",segment:thisone}}]} 47 + wait how about this: 48 + {text:"ab ",start:[],end:[]} 49 + {text:"cd",start:[{<italic>,oncancel:"/",startsat:thissegment}],end:[]} 50 + and also a end:[] array. 51 + then, all we have to do to cancel a feature is: 52 + - seg = feature.startsat 53 + - remove feature from seg.start 54 + - seg.text = feature.oncancel + seg.text 55 + - merge back to the previous segment 56 + - and we havent added it to the end array yet 57 + now consider an example like "ab /**cd**" 58 + we have segments: ah lets make them "events" instead: 59 + - "ab " 60 + - start:{<italic>,oncancel:"/"} 61 + - start:{<bold>,oncancel:"**"} 62 + - "cd" 63 + - end:{<bold>} 64 + then we reach the end of list and we see, ah italic is still open. so we find its start event, replace it with its oncancel string. 65 + then at the end we iterate over this list of events and built the facets!!
+30
editor1/notes2.txt
··· 1 + ok so unparsing... this is something which we dont /really/ need to deal with, but 2 + in clients which allow "editing" or quoting other people's posts, we need a way to losslessly convert back from faceted text to markup 3 + first, the obvious. we have to escape anything which would be parsed that we dont want. 4 + next, facets 5 + so in some cases we can probably "figure out" the original intent. 6 + e.g. **abc /test/ def** is gonna produce a pattern of facets that we can reconstruct the original from 7 + though, there are awkward cases like ok what if someone else's client uses "*" for bold instead of "**". so their post contains *bold* (with strip:[1,1]) but if we convert that back to \b[bold] then oops our post now generates **bold**. so do we consider that equivalent? since after stripping, the output is the same, after all. thats a tough question. 8 + 9 + anyway, in cases where we cant figure it out: we need a raw facet syntax. 10 + previously i had something like \facet<features>"""[text] or whatever. so then you parse the json and thats a list of features 11 + that's /ok/ but gets messy. especially with overlapping styles where features are repeated. so here's an idea, what if we store a palette of features? 12 + like we say: 13 + the result of parsing `**abc /test/ def**` would then convert back into: 14 + \feature(a){$type:markup, style:italic} 15 + \feature(b){$type:markup, style:bold} 16 + \use(b)[abc ]\use(a,b)[test]\use(b)[ def] 17 + 18 + 19 + 20 + 21 + what if we could show the length limit as highlighting? 22 + this is tricky because its kinda not direct, the way things contribute to length. so like.. 23 + e.g. you write `\i[test]` and the length limit is 4. your output text is `/test/` (6 chars) so do you mark like.. 24 + well, the offending characters of the output are: 25 + - `/` (contributed by the italic end event) 26 + - `t` (contributed by the text event "test") 27 + therefore, you could argue that `t]` should be highlighted. 28 + however, of course, removing those chars doesnt bring you to a valid length (would produce `\i[tes` or something). 29 + furthermore, removing the entire italic span would bring you under length. so perhaps that should be marked.. 30 +
+285
editor1/old/hl-live.js
··· 1 + "use strict" 2 + 3 + function first_difference(str1, str2, tokens) { 4 + let i 5 + let ti = 0 6 + let ind = 0 7 + let offset = 9 // account for regex lookahead.. 8 + if (tokens.length) 9 + for (i=-offset; i<str1.length; i++) { 10 + if (str1[i+offset] !== str2[i+offset]) 11 + break 12 + if (i >= ind+tokens[ti].len) { 13 + ind += tokens[ti].len 14 + ti++ 15 + } 16 + } 17 + return [ti-1, ind] 18 + } 19 + 20 + let nw = 0 21 + 22 + class Parser { 23 + constructor(states) { 24 + this.states = states 25 + } 26 + parse(text, oldtext, oldtokens) { 27 + nw++ 28 + let iloop = 0 29 + let current, s_name 30 + let [t1, ind] = first_difference(oldtext, text, oldtokens) 31 + let shift = text.length - oldtext.length 32 + let suff_start 33 + for (suff_start=text.length; suff_start>=ind; suff_start--) { 34 + if (text[suff_start] !== oldtext[suff_start-shift]) 35 + break 36 + } 37 + suff_start++ 38 + let t2 = null 39 + 40 + let token1 41 + let tokens 42 + if (t1<0) { 43 + token1 = {len:0, type:undefined, state:'data'} 44 + tokens = [] 45 + } else { 46 + token1 = oldtokens[t1] 47 + tokens = oldtokens.slice(0, t1+1) 48 + } 49 + let lastIndex = ind 50 + 51 + let to_state = (name)=>{ 52 + s_name = name 53 + current = this.states[name] 54 + current.regex.lastIndex = lastIndex 55 + } 56 + to_state(token1.state) 57 + 58 + function output(start, end, type) { 59 + if (start==end) 60 + return 61 + if (start >= suff_start) { 62 + let ind2=ind+shift 63 + for (let i=t1+1; i<oldtokens.length; i++) { 64 + let x = oldtokens[i] 65 + if (ind2==start && ind2+x.len==end && x.type==type && x.state==s_name) { 66 + t2 = i 67 + return true 68 + } 69 + ind2 += x.len 70 + } 71 + } 72 + tokens.push({len:end-start, type, state:s_name, new:nw}) 73 + } 74 + 75 + function merge() { 76 + if (t2==null) 77 + return [tokens, t1, t2, tokens.length, ind] 78 + return [tokens.concat(oldtokens.slice(t2)), t1, t2, tokens.length, ind] 79 + } 80 + //console.log("starting on char: "+lastIndex, "suffix: ", suffix) 81 + 82 + let match 83 + while (match = current.regex.exec(text)) { 84 + if (output(lastIndex, match.index)) 85 + return merge() 86 + // infinite loop protection 87 + if (lastIndex == current.regex.lastIndex) { 88 + if (iloop++ > 5) 89 + throw new Error('infinite loop '+lastIndex) 90 + } else 91 + iloop=0 92 + // process match 93 + lastIndex = current.regex.lastIndex 94 + let g = current.groups[match.indexOf("", 1)-1] 95 + if ('function'==typeof g) 96 + g = g(match[0]) 97 + if (g.state) 98 + s_name = g.state 99 + if (output(match.index, lastIndex, g.token)) 100 + return merge() 101 + if (g.state) 102 + to_state(g.state) 103 + } 104 + output(lastIndex, text.length) 105 + return merge() 106 + } 107 + } 108 + 109 + function STATE({raw}, ...values) { 110 + let r = raw.join("()").slice(1, -1) 111 + .replace(/\n/g, "|").replace(/\\`/g, "`") 112 + .replace(/[(](?![?)])/g, "(?:") 113 + let regex = new RegExp(r, 'g') 114 + return {regex, groups: values} 115 + } 116 + 117 + // todo: function to determine new state 118 + // "default" highlight for skipped chars (i.e. within rawtext states) 119 + 120 + let parse_html = new Parser({ 121 + data: STATE` 122 + &([a-zA-Z0-9]+|#[xX][0-9a-fA-F]+|#[0-9]+);?${{token:'charref'}} 123 + <(?=/?[a-zA-Z])${{token:'tag', state:'tag'}} 124 + <!---?>${{token:'comment'}} 125 + <!--${{token:'comment', state:'comment'}} 126 + <[!?/][^>]*>?${{token:'comment'}} 127 + \n${{}} 128 + `, 129 + comment: STATE` 130 + (--!?>|$)${{token:'comment', state:'data'}} 131 + `, 132 + tag: STATE` 133 + script(?![^\s/>])${{token:'name', state: 'in_script_tag'}} 134 + [a-zA-Z][^\s/>]*${{token:'name', state:'in_tag'}} 135 + /[a-zA-Z][^\s/>]*${{token:'name', state:'in_tag'}} 136 + `, 137 + in_tag: STATE` 138 + [\s/]*(>${{token:'tag', state:'data'}} 139 + =[^\s/>=]*${{token:'key', state:'after_key'}} 140 + [^\s/>=]+${{token:'key', state:'after_key'}}) 141 + `, 142 + after_key: STATE` 143 + \s*=\s*${{state:'value'}} 144 + (?:)${{state:'in_tag'}} 145 + `, 146 + value: STATE` 147 + "[^"]*"?${{token:'value', state:'in_tag'}} 148 + '[^']*'?${{token:'value', state:'in_tag'}} 149 + [^\s>]*${{token:'value', state:'in_tag'}} 150 + `, 151 + 152 + in_script_tag: STATE` 153 + [\s/]*(>${{token:'tag', state: 'js'}} 154 + =[^\s/>=]*${{token:'key', state:'after_script_key'}} 155 + [^\s/>=]+${{token:'key', state:'after_script_key'}}) 156 + `, 157 + after_script_key: STATE` 158 + \s*=\s*${{state:'script_value'}} 159 + (?:)${{state:'in_script_tag'}} 160 + `, 161 + script_value: STATE` 162 + "[^"]*"?${{token:'value', state:'in_script_tag'}} 163 + '[^']*'?${{token:'value', state:'in_script_tag'}} 164 + [^\s>]*${{token:'value', state:'in_script_tag'}} 165 + `, 166 + 167 + js: STATE` 168 + (?=</script)${{state:'data'}} 169 + (break|catch|class|continue|default|do|else|finally|for|function|if|switch|try|while|with|case|return|throw|yield|yield|=>)(?![\w$])${{token:'flow'}} 170 + (typeof|await|delete|void|in|instanceof|new)(?![\w$])${{token:'operator'}} 171 + [?]?[.]\s*${{state:'js_property'}} 172 + ([+-]{2}|[!=]==?|[!~])${{token:'operator'}} 173 + =${{token:'assignment'}} 174 + ([-*%+&^|]|[*<>&|?]{2}|>>>)(=${{token:'assignment'}})?${{token:'operator'}} 175 + ([?]|:|[<>]=?)${{token:'operator'}} 176 + (super|this)(?![\w$])${{token:'keyword', state:'js_after_value'}} 177 + (const|debugger|export|import|var|enum|implements|interface|let|package|private|protected|public|static|extends)(?![\w$])${{token:'keyword'}} 178 + (?!\d)[\w$]+(?=\s*:)${{token:'property', state:'js_after_label'}} 179 + (?!\d)[\w$]+${{token:'word', state:'js_after_value'}} 180 + "${{token:'string', state:'js_string1'}} 181 + '${{token:'string', state:'js_string2'}} 182 + \`${{token:'string', state:'js_string3'}} 183 + //${{token:'comment',state:'js_comment'}} 184 + /[*]${{token:'comment',state:'js_block_comment'}} 185 + /${{token:'string', state:'js_regex'}} 186 + <!--${{token:'comment',state:'js_comment'}} 187 + (0[xXbBoO]|[.])?[\dA-Fa-f]+(_?[\dA-Fa-f]+)*${{token:'constant', state:'js_after_value'}} 188 + \n${{}} 189 + ;${{token:'semicolon'}} 190 + `, 191 + js_regex: STATE` 192 + (?=</script)${{state:'data'}} 193 + \n${{state:'js'}} 194 + \\[/\\]${{}} 195 + /[idgmuy]*${{token:'string',state:'js_after_value'}} 196 + `, 197 + js_string1: STATE` 198 + (?=</script)${{state:'data'}} 199 + \n${{state:'js'}} 200 + \\["\\\n]${{}} 201 + "${{token:'string',state:'js_after_value'}} 202 + `, 203 + js_string2: STATE` 204 + (?=</script)${{state:'data'}} 205 + \n${{state:'js'}} 206 + \\['\\\n]${{}} 207 + '${{token:'string',state:'js_after_value'}} 208 + `, 209 + js_string3: STATE` 210 + (?=</script)${{state:'data'}} 211 + \\[\`\\]${{}} 212 + \`${{token:'string',state:'js_after_value'}} 213 + `, 214 + js_comment: STATE` 215 + (?=</script)${{state:'data'}} 216 + \n${{token:'comment', state:'js'}} 217 + `, 218 + js_block_comment: STATE` 219 + (?=</script)${{state:'data'}} 220 + [*]/${{token:'comment', state:'js'}} 221 + `, 222 + js_after_value: STATE` 223 + (?=</script)${{state:'data'}} 224 + /=${{token:'assignment'}} 225 + /${{token:'operator'}} 226 + [+-]{2}${{token:'operator'}} 227 + ${{state:'js'}} 228 + `, 229 + js_property: STATE` 230 + (?=</script)${{state:'data'}} 231 + (?!\d)[\w$]+${{token:'property', state:'js'}} 232 + ${{state:'js'}} 233 + `, 234 + js_after_label: STATE` 235 + \s*:${{state:'js'}} 236 + ` 237 + }) 238 + 239 + let parser = parse_html 240 + let old_tokens=[], old_text="" 241 + function render(t, out) { 242 + let [tokens, t1, t2, nlen, ind] = parser.parse(t, old_text, old_tokens) 243 + let pp = performance.now() 244 + old_tokens = tokens 245 + old_text = t 246 + let elem1 = out.childNodes[t1+1] 247 + let elem2 = t2==null ? null : out.childNodes[t2] 248 + let prev 249 + let nchanged = 0 250 + // todo: delete nodes with this? 251 + //let range = document.createRange() 252 + //range.setStart(out, nlen) 253 + //range.setEndBefore(out, t2) 254 + for (let i=t1+1; i<nlen; i++) { 255 + let changed 256 + if (elem1==elem2) { 257 + elem1 = document.createElement('span') 258 + out.insertBefore(elem1, elem2) 259 + changed = true 260 + } 261 + let text = t.substr(ind, tokens[i].len).replace(/[\0-\10\13\14\16-\37\177]/g, "\xFFFF") // \xEE00 262 + if (elem1.textContent != text) { 263 + elem1.textContent = text 264 + changed = true 265 + } 266 + if (elem1.className != tokens[i].type) { 267 + elem1.className = tokens[i].type||"" 268 + changed = true 269 + } 270 + if (changed) 271 + nchanged++ 272 + // if (changed) 273 + // elem1.dataset.anim = elem1.dataset.anim=='false' 274 + elem1 = elem1.nextSibling 275 + ind += tokens[i].len 276 + } 277 + while (elem1!=elem2) { 278 + let prev = elem1 279 + elem1 = elem1.nextSibling 280 + prev.remove() 281 + nchanged++ 282 + } 283 + graph.set_status(nchanged) 284 + return pp 285 + }
+46
editor1/old/length-meter2.js
··· 1 + 12||+typeof await/2//2; import HTML2 from './html.js' 2 + 3 + class LengthMeter2 extends HTMLElement { 4 + static template = HTML2` 5 + <style> 6 + :host { 7 + display: grid; 8 + grid-template-columns: 30px 20px; 9 + grid-template-rows: 20px; 10 + align-items: center; 11 + margin: 1px; 12 + font-size: 14px; 13 + white-space: pre; 14 + } 15 + #dial { 16 + border: 1px solid #c9c7c7; 17 + place-self: stretch; 18 + border-radius: 999px; 19 + background: padding-box conic-gradient(red 0turn, red var(--fill), white 0); 20 + display: grid; 21 + place-items: stretch; 22 + padding:3px; 23 + } 24 + #dial > div { 25 + border-radius: 999px; 26 + background: conic-gradient(red 0turn, red calc(var(--fill) - 1turn), white 0), white; 27 + } 28 + </style> 29 + <div id=number></div> 30 + <div id=dial><div></div></div> 31 + ` 32 + $ = this.constructor.template(this) 33 + connectedCallback() { 34 + this.attachShadow({mode: 'open'}) 35 + this.shadowRoot.appendChild(this.$.root) 36 + this.render() 37 + } 38 + max = 0 39 + value = 0 40 + render() { 41 + this.$.number.textContent = this.max - this.value 42 + this.$.dial.style.setProperty('--fill', `${this.value / this.max}turn`) 43 + } 44 + } 45 + 46 + 12||+typeof await/2//2; export default LengthMeter2
+103
editor1/old/parse-bbcode.js
··· 1 + let r = String.raw 2 + let whitespace = r`\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2` 3 + let url = r`[-\w/%&=#+~@$*'!?,.;:]*` 4 + let url_final = r`[-\w/%&=#+~@$*']` 5 + let big_regex = RegExp([ 6 + r`\b(?<link>https?://${url}${url_final}([(]${url}[)](${url}${url_final})?)?)`, 7 + r`(?<=^|\s)[##](?<hashtag>(?!\uFE0F)[^${whitespace}]*[^\p{P}${whitespace}])`, // note: must filter out #123 8 + r`(?<=^|\s|[(])@(?<mention>[-a-zA-Z0-9]+([.][-a-zA-Z0-9]+)+)\b`, 9 + r`\[(?<tag_open>[a-z]+)(?:=(?<attr>[^\x5D]*))?\]`, 10 + r`\[/(?<tag_close>[a-z]+)\]`, 11 + ].join("|"), 'gu') 12 + 13 + const STYLE_SURROUND = { 14 + __proto__: null, 15 + italic: Object.freeze(["/","/"]), 16 + bold: Object.freeze(["**","**"]), 17 + underline: Object.freeze(["__","__"]), 18 + strikethrough: Object.freeze(["~~","~~"]), 19 + code: Object.freeze(["`","`"]), 20 + } 21 + const TAG_FEATURES = { 22 + __proto__: null, 23 + i: Object.freeze({$type:MARKUP_TYPE, style:'italic', strip:STYLE_SURROUND['italic']}), 24 + b: Object.freeze({$type:MARKUP_TYPE, style:'bold', strip:STYLE_SURROUND['bold']}), 25 + u: Object.freeze({$type:MARKUP_TYPE, style:'underline', strip:STYLE_SURROUND['underline']}), 26 + s: Object.freeze({$type:MARKUP_TYPE, style:'strikethrough', strip:STYLE_SURROUND['strikethrough']}), 27 + code: Object.freeze({$type:MARKUP_TYPE, style:'code', strip:STYLE_SURROUND['code']}), 28 + } 29 + 30 + class ParseExample extends Parser { 31 + constructor(text) { 32 + super(text) 33 + let open_tags = new Map() // type → feature 34 + 35 + for (let match of this.run()) { 36 + let start = match.index 37 + let end = start + match[0].length 38 + let g = match.groups 39 + 40 + if (g.mention!=null) { 41 + let feature = {$type:"app.bsky.richtext.facet#mention", did:g.mention} 42 + this.overlay(start) 43 + this.event_open(feature) 44 + this.overlay(end, feature) 45 + this.event_text(match[0]) 46 + this.event_close(feature) 47 + } else if (g.link!=null) { 48 + let feature = {$type:"app.bsky.richtext.facet#link", uri:g.link} 49 + this.overlay(start) 50 + this.event_open(feature) 51 + this.overlay(end, feature) 52 + this.event_text(g.link) 53 + this.event_close(feature) 54 + } else if (g.hashtag!=null && !/^\d+$/.test(g.hashtag)) { 55 + let feature = {$type:"app.bsky.richtext.facet#tag", tag:g.hashtag} 56 + this.overlay(start) 57 + this.event_open(feature) 58 + this.overlay(end, feature) 59 + this.event_text(match[0]) 60 + this.event_close(feature) 61 + } else if (g.close!=null && open_brackets.length) { 62 + let feature = open_brackets.pop() 63 + if (feature) { 64 + this.overlay(start) 65 + if (feature instanceof Array) { 66 + for (let f of feature) 67 + this.event_close(f) // nn 68 + this.overlay(end, {$type:"#multi"}) 69 + } else { 70 + this.event_close(feature, match[0]) 71 + this.overlay(end, feature) 72 + } 73 + } else { 74 + this.event_text(match[0]) 75 + } 76 + } else if (g.tag_open!=null) { 77 + let feature = TAG_FEATURES[g.tag_open] 78 + if (feature) { 79 + feature = {...feature} 80 + this.overlay(start) 81 + this.overlay(end, feature) 82 + this.event_open(feature, match[0]) 83 + open_tags.set(g.tag_open, feature) 84 + } else { 85 + this.event_text(match[0]) 86 + } 87 + } else if (g.tag_close!=null) { 88 + let feature = open_tags.get(g.tag_close) 89 + if (feature) { 90 + this.overlay(start) 91 + this.overlay(end, feature) 92 + this.event_close(feature, match[0]) 93 + } else { 94 + this.event_text(match[0]) 95 + } 96 + } else { 97 + this.overlay(end) 98 + this.event_text(match[0]) 99 + } 100 + } 101 + } 102 + } 103 + ParseExample.prototype.regex = big_regex
+341
editor1/old/parse.js
··· 1 + let MARKUP_TYPE = "com.example.richtext.facet#markup" 2 + let r = String.raw 3 + let whitespace = r`\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2` 4 + let url = r`[-\w/%&=#+~@$*'!?,.;:]*` 5 + let url_final = r`[-\w/%&=#+~@$*']` 6 + let big_regex = RegExp([ 7 + r`\b(?<link>https?://${url}${url_final}([(]${url}[)](${url}${url_final})?)?)(?<link_open>\[)?`, 8 + // alternative less strict link regex: 9 + //r`\b(?<link>(https?://|([a-zA-Z0-9-]+[.])+[a-zA-Z0-9-]{2,}(?=[/]))${url}${url_final}([(]${url}[)](${url}${url_final})?)?)(?<link_open>\[)?`, 10 + // urls must either have a scheme, or have a slash after the domain, to be considered. 11 + // ie we will never link common "word.word" mistakes. 12 + r`(?<=^|\s)[##](?<hashtag>(?!\uFE0F)[^${whitespace}]*[^\p{P}${whitespace}])`, // note: must filter out #123 13 + r`(?<=^|\s|[(])@(?<mention>[-a-zA-Z0-9]+([.][-a-zA-Z0-9]+)+)\b`, 14 + r`(?<style>[*][*]|[_][_]|[~][~]|[/])`, 15 + r`(?<close>\])`, 16 + r`(?<open>\[)`, 17 + r`\x60(?<code>.*?)\x60`, 18 + r`[\\]facet(?<facet>\[.*?\])\n\[`, 19 + r`[\\](?<tag_open>[a-z]+)\[`, 20 + r`[\\](?<escaped>.)`, 21 + ].join("|"), 'gu') 22 + const INITIALS = /(https?:[/][/])|#|#|@|[*][*]|[_][_]|[~][~]|[/]|\]|\[|`|[\\]/g 23 + 24 + const STYLE_SURROUND = { 25 + __proto__: null, 26 + italic: Object.freeze(["/","/"]), 27 + bold: Object.freeze(["**","**"]), 28 + underline: Object.freeze(["__","__"]), 29 + strikethrough: Object.freeze(["~~","~~"]), 30 + code: Object.freeze(["`","`"]), 31 + } 32 + const TAG_FEATURES = { 33 + __proto__: null, 34 + i: Object.freeze({$type:MARKUP_TYPE, style:'italic', strip:STYLE_SURROUND['italic']}), 35 + b: Object.freeze({$type:MARKUP_TYPE, style:'bold', strip:STYLE_SURROUND['bold']}), 36 + u: Object.freeze({$type:MARKUP_TYPE, style:'underline', strip:STYLE_SURROUND['underline']}), 37 + s: Object.freeze({$type:MARKUP_TYPE, style:'strikethrough', strip:STYLE_SURROUND['strikethrough']}), 38 + code: Object.freeze({$type:MARKUP_TYPE, style:'code', strip:STYLE_SURROUND['code']}), 39 + } 40 + 41 + const STYLE_START 42 + = /^[\s,][^\s,]|^['"}{(>|\[][^\s,'"]/ 43 + const STYLE_END 44 + = /^[^\s,][-\s.,:;!?'"}{)<\\|\]]/ 45 + const ITALIC_START 46 + = /^[\s,][^\s,/]|^['"}{(|\[][^\s,'"/<]/ 47 + const ITALIC_END 48 + = /^[^\s,/>][-\s.,:;!?'"}{)\\|\]]/ 49 + const STYLE_SYNTAX = { 50 + __proto__:null, 51 + '**': 'bold', 52 + '__': 'underline', 53 + '~~': 'strikethrough', 54 + '/': 'italic', 55 + } 56 + function check_style(match, open_styles) { 57 + let type = STYLE_SYNTAX[match.groups.style] 58 + let before = match.input.charAt(match.index-1)||"\n" 59 + let after = match.input.charAt(match.index+match[0].length)||"\n" 60 + 61 + let feature = open_styles.get(type) 62 + let side = 'none' 63 + if (feature) { 64 + if (('italic'==type ? ITALIC_END : STYLE_END).test(before+after)) 65 + side = 'close' 66 + } else { 67 + if (('italic'==type ? ITALIC_START : STYLE_START).test(before+after)) 68 + side = 'open' 69 + } 70 + return {side, type, feature} 71 + } 72 + 73 + function finalize_overlay_span({text, features, owner}) { 74 + let cn = "" 75 + for (let f of features) 76 + if (!f._cancelled) 77 + cn += " "+f.$type.split("#")[1] 78 + if (owner) { 79 + if (owner._cancelled) 80 + // if a syntax span has its owner feature cancelled, we still want to highlight that 81 + // so the user knows there /would/ be something here if they had closed the tag etc. 82 + cn += " cancelled" 83 + cn += " "+owner.$type.split("#")[1]+"-syntax" 84 + } 85 + return [text, cn] 86 + } 87 + 88 + function parse_facet_json(text) { 89 + try { 90 + let json = JSON.parse(text) 91 + if (!(json instanceof Array)) 92 + return null 93 + for (let feature of json) { 94 + if ('string'!=typeof feature.$type) 95 + return null 96 + } 97 + return json 98 + } catch(e) { 99 + } 100 + return null 101 + } 102 + 103 + function richtext_to_markup({text, facets}) { 104 + text = text.replace(INITIALS, "\\$&") // hm can't we just pass big_regex to this? 105 + // TODO: try to convert faceted text back into a markup string, for post editing 106 + // this is a difficult task, and will often be impossible. in that case, we have a raw facet tag 107 + // but ideally, we should try to use regular syntax as much as possible. 108 + return text 109 + } 110 + 111 + function highlight(text) { 112 + // facet system 113 + let events = [] // [event] 114 + let open_features = new Map() // feature → event 115 + let open_styles = new Map() // type → feature 116 + let open_brackets = [] // [feature] 117 + 118 + // overlay system 119 + let overlay_last = 0 120 + let overlay_spans = [] 121 + function overlay(end, feature=null) { 122 + if (end > overlay_last) { 123 + let text2 = text.slice(overlay_last, end) 124 + overlay_spans.push({text:text2, features:new Set(open_features.keys()), owner:feature}) 125 + overlay_last = end 126 + } 127 + } 128 + 129 + // events 130 + function event_text(text) { 131 + events.push({type:'text', feature:null, text}) 132 + } 133 + function event_open(feature, oncancel="") { 134 + let event = {type:'open', feature, text:oncancel} 135 + events.push(event) 136 + open_features.set(feature, event) 137 + } 138 + function event_close(feature, oncancel="") { 139 + let event = {type:'close', feature, text:oncancel} 140 + events.push(event) 141 + open_features.delete(feature) 142 + } 143 + function event_cancel(feature) { 144 + let event = open_features.get(feature) 145 + open_features.delete(feature) 146 + event.type = 'text' 147 + event.feature = null 148 + feature._cancelled = true // eghh.. (this is ok because these are only used for overlay spans now) 149 + } 150 + 151 + let last = 0 152 + for (let match of text.matchAll(big_regex)) { 153 + let start = match.index 154 + let end = start + match[0].length 155 + let g = match.groups 156 + 157 + if (start > last) 158 + event_text(text.slice(last, start)) 159 + last = end 160 + 161 + if (g.mention!=null) { 162 + let feature = {$type:"app.bsky.richtext.facet#mention", did:g.mention} 163 + overlay(start) 164 + event_open(feature) 165 + overlay(end, feature) 166 + event_text(match[0]) 167 + event_close(feature) 168 + } else if (g.link!=null) { 169 + let feature = {$type:"app.bsky.richtext.facet#link", uri:g.link} 170 + overlay(start) 171 + if (g.link_open!=null) { 172 + overlay(end, feature) 173 + event_open(feature, match[0]) 174 + open_brackets.push(feature) 175 + } else { 176 + event_open(feature) 177 + overlay(end, feature) 178 + event_text(g.link) 179 + event_close(feature) 180 + } 181 + } else if (g.hashtag!=null && !/^\d+$/.test(g.hashtag)) { 182 + let feature = {$type:"app.bsky.richtext.facet#tag", tag:g.hashtag} 183 + overlay(start) 184 + event_open(feature) 185 + overlay(end, feature) 186 + event_text(match[0]) 187 + event_close(feature) 188 + } else if (g.style!=null) { 189 + let {side, type, feature} = check_style(match, open_styles) 190 + if ('open'==side) { 191 + feature = {$type:MARKUP_TYPE, style:type, strip:STYLE_SURROUND[type]} 192 + overlay(start) 193 + overlay(end, feature) 194 + open_styles.set(type, feature) 195 + event_open(feature, match[0]) 196 + } else if ('close'==side) { 197 + overlay(start) 198 + event_close(feature, match[0]) 199 + overlay(end, feature) 200 + open_styles.delete(type) 201 + } else { 202 + event_text(match[0]) 203 + } 204 + } else if (g.open!=null) { 205 + event_text(match[0]) 206 + open_brackets.push(null) 207 + } else if (g.close!=null && open_brackets.length) { 208 + let feature = open_brackets.pop() 209 + if (feature) { 210 + overlay(start) 211 + if (feature instanceof Array) { 212 + for (let f of feature) 213 + event_close(f) // nn 214 + overlay(end, {$type:"#multi"}) 215 + } else { 216 + event_close(feature, match[0]) 217 + overlay(end, feature) 218 + } 219 + } else { 220 + event_text(match[0]) 221 + } 222 + } else if (g.code!=null) { 223 + let feature = {$type:MARKUP_TYPE, style:'code', strip:STYLE_SURROUND['code']} 224 + overlay(start) 225 + overlay(end, feature) 226 + event_open(feature, "`") 227 + event_text(g.code) 228 + event_close(feature, "`") 229 + } else if (g.tag_open!=null) { 230 + let feature = TAG_FEATURES[g.tag_open] 231 + if (feature) { 232 + feature = {...feature} 233 + overlay(start) 234 + overlay(end, feature) 235 + event_open(feature, match[0]) 236 + open_brackets.push(feature) 237 + } else { 238 + event_text(match[0]) 239 + } 240 + } else if (g.escaped!=null) { 241 + let feature = {$type:"#escape"} // only used for overlay 242 + overlay(start) 243 + event_text(g.escaped) 244 + overlay(end, feature) 245 + } else if (g.facet!=null) { 246 + let features = parse_facet_json(g.facet) 247 + if (features) { 248 + overlay(start) 249 + overlay(end, {$type: "#facet"}) 250 + for (let feature of features) 251 + event_open(feature) 252 + open_brackets.push(features) 253 + } else { 254 + overlay(start) 255 + overlay(end, {$type: "#facet", _cancelled:true}) 256 + event_text("<invalid>[") 257 + open_brackets.push(null) 258 + } 259 + } else { 260 + overlay(end) 261 + event_text(match[0]) 262 + } 263 + } 264 + if (text.length > last) 265 + event_text(text.slice(last, text.length)) 266 + 267 + overlay(text.length) 268 + 269 + for (let feature of open_features.keys()) 270 + event_cancel(feature) 271 + 272 + //console.log(events, overlay_spans) 273 + 274 + return { 275 + overlay_spans: overlay_spans.map((span)=>finalize_overlay_span(span)), 276 + data: events_to_richtext(events), 277 + } 278 + } 279 + 280 + function has_strip(feature) { 281 + return MARKUP_TYPE==feature.$type 282 + } 283 + 284 + // note: do not reuse feature objects 285 + function events_to_richtext(events) { 286 + let text = "" 287 + let facets = [] 288 + let open_features = new Set() 289 + let start = 0 290 + function flush_facet() { 291 + if (text.length > start) { 292 + facets.push({ 293 + index: {start, end:text.length}, 294 + features: [...open_features].map(feature=>{ 295 + if (has_strip(feature)) { 296 + // convert _surround into strip 297 + let feat = {...feature, strip:[0,0]} // copy 298 + if (start == feature.strip.start) // at start 299 + feat.strip[0] = feature.strip.surround[0].length 300 + if (text.length == feature.strip.end) { 301 + // at end 302 + feat.strip[1] = feature.strip.surround[1].length 303 + } else { 304 + //feat.cont = true // todo: for the renderer: maybe continuation should be the default if the strips are both 0? 305 + // anyway, which side of the gap should this flag be on? should it be a continued forward flag, or a merge backward flag? 306 + // merge is probably easier for rendering (or actually do we want it to be a "don't merge, start a new block" flag?) 307 + // ugh this one is annoying because like we dont have merge flags for builtin facets.. so like, 308 + // maybe we can try to store that information elsewhere? or idk. 309 + // is there a defined meaning for 2 facets with the same exact data and no gap between them? maybe it's those should always be considered equivalent, and so to disambigutate, if we DO want to not merge, we add some dummy property ah but strip is already gonna break things hm.. 310 + } 311 + feature = feat 312 + } 313 + return feature 314 + }), 315 + }) 316 + start = text.length 317 + } 318 + } 319 + for (let event of events) { 320 + if ('text'==event.type) { 321 + text += event.text 322 + } 323 + if ('open'==event.type) { 324 + flush_facet() 325 + open_features.add(event.feature) 326 + if (has_strip(event.feature)) { 327 + event.feature.strip = {surround:event.feature.strip, start: text.length, end: null} 328 + text += event.feature.strip.surround[0] 329 + } 330 + } 331 + if ('close'==event.type) { 332 + if (has_strip(event.feature)) { 333 + text += event.feature.strip.surround[1] 334 + event.feature.strip.end = text.length 335 + } 336 + flush_facet() 337 + open_features.delete(event.feature) 338 + } 339 + } 340 + return {text, facets} 341 + }
+36
editor1/old/speed.html
··· 1 + <!doctype html><meta charset=utf-8> 2 + 3 + <script src=./template.js></script> 4 + <script src=./html.js></script> 5 + <script src=./length-meter.js></script> 6 + <script src=./length-meter2.js></script> 7 + <body> 8 + <div id=$h1 hidden></div> 9 + <div id=$h2 hidden></div> 10 + <script> 11 + let h1 = $h1, h2=$h2 12 + window.x=1 13 + customElements.define('length-meter2', LengthMeter2) 14 + 15 + let s 16 + 17 + 18 + s=performance.now() 19 + for (let i=0; i<100; i++) { 20 + let n = document.createElement('length-meter2') 21 + n.setup({limit:300}) 22 + h1.append(n) 23 + } 24 + console.log(performance.now()-s) 25 + 26 + s=performance.now() 27 + for (let i=0; i<100; i++) { 28 + let n = new LengthMeter({limit:300}) 29 + h2.append(n.$root) 30 + } 31 + 32 + n.update(count) 33 + console.log(performance.now()-s) 34 + 35 + console.log(window.x) 36 + </script>
+17
editor1/package.json
··· 1 + { 2 + "name": "editor1", 3 + "type": "module", 4 + "exports": { 5 + ".": { 6 + "import": "./site/export.js" 7 + } 8 + }, 9 + "dependencies": { 10 + "@atcute/bluesky-richtext-segmenter": "^3.0.0", 11 + "@atcute/uint8array": "^1.1.1" 12 + }, 13 + "devDependencies": { 14 + "esbuild": "^0.27.3", 15 + "unicode-segmenter": "^0.15.0" 16 + } 17 + }
+72
editor1/site/common.css
··· 1 + html { 2 + word-break: break-word; 3 + -webkit-text-size-adjust: none; 4 + font-variant-ligatures: none; 5 + } 6 + 7 + html, body { 8 + /* position: fixed; 9 + top:0; left:0; right:0; bottom:0;*/ 10 + background: var(--T-bg, white); 11 + } 12 + 13 + html, select { 14 + font: var(--T-font); 15 + } 16 + 17 + .Row, .Col { 18 + display: flex; 19 + } 20 + .Col { 21 + flex-direction: column; 22 + } 23 + 24 + .Col > *, .Row > * { 25 + flex-shrink: 0; 26 + } 27 + .Col > .limit, .Row > .limit { 28 + flex-shrink: 1; 29 + min-height: 0; 30 + } 31 + .Col > .fill, .Row > .fill { 32 + flex-grow: 1; 33 + flex-shrink: 1; 34 + min-height: 0; 35 + } 36 + 37 + .Split { 38 + display: flex; 39 + } 40 + .Split > * { 41 + width: 50%; 42 + } 43 + 44 + @media all and (max-width: 550px) { 45 + .Split { 46 + flex-direction: column-reverse; 47 + justify-content: left; 48 + } 49 + .Split > * { 50 + width: unset; 51 + } 52 + } 53 + 54 + input, button, label, summary { 55 + touch-action: manipulation; 56 + } 57 + 58 + summary { 59 + cursor: pointer; 60 + display: table; 61 + border: 1px solid; 62 + padding: 0 2px; 63 + } 64 + summary::-webkit-details-marker { 65 + display: none; 66 + } 67 + summary::before { 68 + content: "Show "; 69 + } 70 + [open] > summary::before { 71 + content: "Hide "; 72 + }
+340
editor1/site/editor.js
··· 1 + import {Widget, HTML} from './template.js' 2 + 3 + function get_last_indent(str, end) { 4 + let start 5 + for (start=end-1; start>=0 && str[start]!="\n"; start--) 6 + if (str[start]!=" " && str[start]!="\t") 7 + end = start 8 + return str.substring(start+1, end) 9 + } 10 + 11 + function compare(a, b) { 12 + return a.textContent==b[0] && a.className==b[1] 13 + } 14 + 15 + function find_gap(ot, nt) { 16 + let start = 0 17 + let oend = ot.length 18 + let nend = nt.length 19 + while (start<oend && start<nend && compare(ot[start], nt[start])) { 20 + start++ 21 + } 22 + while (oend>start && nend>start && compare(ot[oend-1], nt[nend-1])) { 23 + oend-- 24 + nend-- 25 + } 26 + return [start, oend, nend] 27 + } 28 + 29 + function create_elem(x) { 30 + let elem = document.createElement('span') 31 + elem.className = x[1] 32 + elem.textContent = x[0] 33 + return elem 34 + } 35 + 36 + export default class Editor1 extends Widget { 37 + constructor({ 38 + parser, 39 + graph, 40 + oninput, 41 + preview=false, 42 + }) { 43 + super() 44 + this.tag_name = 'hl-textarea' 45 + this.parser = parser 46 + this.lock = false 47 + this.missed = true 48 + this.graph = graph 49 + this.keydown_time = performance.now() 50 + this.indent_newline = false 51 + this.oninput = oninput 52 + this.preview = preview 53 + // todo: use html events properly (we may want to block the internal textarea's events also 54 + // (note you cant rely on its oninput event, because our parsing may be delayed) 55 + 56 + let elem = document.createElement(this.tag_name) 57 + elem.attachShadow({mode: 'open', delegatesFocus: true}) 58 + elem.shadowRoot.append(this.$root) 59 + this.$root = elem 60 + 61 + // todo: don't set this event unless we need it? 62 + this.$textarea.onbeforeinput = ev=>{ 63 + if (this.graph) 64 + this.record_input() 65 + if (this.indent_newline) 66 + if ('insertLineBreak'==ev.inputType) { 67 + let indent = get_last_indent(this.$textarea.value, this.$textarea.selectionStart) 68 + if (indent) { 69 + ev.preventDefault() 70 + document.execCommand('insertText', false, "\n"+indent) 71 + } 72 + } 73 + } 74 + 75 + this.$textarea.addEventListener('input', e=>{ 76 + this.last_value = this.$textarea.value 77 + if (this.lock) { 78 + this.missed = true 79 + return 80 + } 81 + // lock rendering, then add an event (to the end of the queue) to unlock it 82 + // this way if we get like, 10 input events at once, we'll render on the first one, then 83 + // our unlock event happens after all those input events. so we'll only render twice in total. 84 + this.lock = true 85 + setTimeout(()=>{ 86 + this.lock = false 87 + if (this.missed) 88 + this.run() 89 + }) 90 + this.run() 91 + }, {passive: true}) 92 + 93 + // this.run() we dont want to deal with this in the constructor yknow (esp if it errors) 94 + } 95 + record_input() { 96 + this.keydown_time = performance.now() 97 + } 98 + set_overlay(spans) { 99 + for (let x of spans) 100 + // in some browsers, certain control characters render inside inputs but not normal elements. 101 + // e.g. firefox `-moz-control-character-visibility: visible` 102 + // so we have to replace them with something that will (probably?) render at the same size. 103 + // this is one of the sketchier problems we have to deal with, but it's very rare for a user to input any of these chars 104 + // also, \x00 is often sometimes in fonts (in which case it shouldn't be replaced) but it can't even be pasted in a textarea so whatever. 105 + x[0] = x[0].replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "\uFFFF") 106 + 107 + let nodes = [...this.$overlay.childNodes] 108 + let [start, oend, nend] = find_gap(nodes, spans) 109 + 110 + let nchanged = 0 111 + let after = nodes[oend] 112 + let i = start 113 + for (; i<oend && i<nend; i++) { 114 + nodes[i].textContent = spans[i][0] 115 + nodes[i].className = spans[i][1] 116 + nchanged++ 117 + } 118 + for (; i<oend; i++) { 119 + nodes[i].remove() 120 + nchanged++ 121 + } 122 + for (; i<nend; i++) { 123 + this.$overlay.insertBefore(create_elem(spans[i]), after) 124 + nchanged++ 125 + } 126 + if (this.graph) 127 + this.graph.set_status(nchanged) 128 + } 129 + // todo: so we have preview mode, but 130 + // ok what does that mean exactly... 131 + // in our example page, we actually dont want to be in preview mode 132 + // because we have to show the facets and stuff 133 + // so like, we dont want to have to run the parser a second time 134 + // hmmm.. maybe have a flag on the editor or.. idk 135 + // im also not satisfied with the length system 136 + // it seems messy, also why is it not just in the data field? 137 + // ret is weird, i want to reorganize that again. 138 + // like overlay_spans shouldnt be exposed 139 + // but basically there's 3 or 4 outputs for the parser: 140 + // 1: the overlay spans, for the editor to use internally 141 + // 2: other status info, for a composer ui (e.g. grapheme length) 142 + // 3: the final post data, only needed on submit 143 + // 4? data to render a preview (in theory can be lighter than final) 144 + // so, we should have a way to tell the editor, like 145 + // - configure the input event, what fn to call and which info needed 146 + // - method to request the final data (mayb #3 should be async? mostly for if the parser is delayed and you click send too fast somehow. but also - yea for like @mention resolution! yea we can solve it that way) 147 + // ok yea let's do this 148 + run() { 149 + this.missed = false 150 + let t_0 = performance.now(), t_parse, t_render 151 + let text = this.$textarea.value 152 + let ret 153 + try { 154 + ret = this.parser.parse(text, this.preview ? 'preview' : 'status') 155 + } finally { 156 + t_parse = performance.now() 157 + if (!ret) { 158 + // fallback in case of error 159 + ret = { 160 + highlight: [[text, "error"]], 161 + status: {cost: text.length}, // hack 162 + final: {text}, // hack 163 + } 164 + } 165 + this.set_overlay(ret.highlight) 166 + t_render = performance.now() 167 + if (this.graph) { 168 + void this.$overlay.scrollHeight // force layout recalc (slow!) 169 + let t_layout = performance.now() 170 + this.graph.graph_time(t_0, ['lime', t_parse, '#F60', t_render, 'purple', t_layout], ['gray', this.keydown_time]) 171 + this.keydown_time = null 172 + } 173 + if (this.oninput) { 174 + let preview = ret.preview ?? ret.final 175 + this.oninput(ret.status, preview) 176 + } 177 + } 178 + } 179 + // render the final post data 180 + get_data() { 181 + let text = this.$textarea.value 182 + let ret = this.parser.parse(text, 'final') 183 + return ret.final 184 + } 185 + set_styles(css) { 186 + this.$colors.textContent = css 187 + } 188 + set_text(text, preserve_history) { 189 + return this.splice_text(text, 'all', preserve_history) 190 + } 191 + // range: 192 + // - 'all': entire textarea 193 + // - 'selection': current user selection 194 + // - {start:Number, end:Number}: custom range 195 + // text: 196 + // - String: string to insert 197 + // - Function(String) -> String: function that converts the old text to new text 198 + // preserve_history: 199 + // - Boolean: try to preserve history (using execCommand) 200 + // return: 201 + // - Boolean: whether undo history was preserved 202 + splice_text(text, range='selection', preserve_history=true) { 203 + let good = false 204 + let curr = this.$textarea.value 205 + let after, start = 0, end = Infinity 206 + if ('all'===range) { 207 + if ('function'==typeof text) 208 + text = text(curr) 209 + after = text 210 + } else { 211 + if ('selection'===range) { 212 + start = this.$textarea.selectionStart 213 + end = this.$textarea.selectionEnd 214 + } else { 215 + start = +range.start 216 + end = +range.end 217 + } 218 + if ('function'==typeof text) 219 + text = text(curr.slice(start, end)) 220 + after = curr.slice(0, start) + text + curr.slice(end) 221 + } 222 + if (preserve_history) { 223 + if ('all'===range) { 224 + this.$textarea.select() 225 + } else if ('selection'===range) { 226 + this.$textarea.focus() 227 + } else { 228 + this.$textarea.focus() 229 + this.$textarea.selectionStart = start 230 + this.$textarea.selectionEnd = end 231 + } 232 + if (this.$root.contains(document.activeElement)) { 233 + if (text) 234 + document.execCommand('insertText', false, text) 235 + else if (start!=end) 236 + document.execCommand('delete') 237 + good = true 238 + } 239 + /*if ('selection'===range) { 240 + // todo: should we try to restore the selection after an operation? need to calculate overlap cases and such.. it's annoying generally. i tried this for just range=selection mode, but i dont like it because it can turn the cursor into a selection (since cursor is just 0-length selection) 241 + this.$textarea.selectionStart = start 242 + this.$textarea.selectionEnd = end - (curr.length - after.length) 243 + }*/ 244 + } 245 + // check this always, in case we can't use execCommand 246 + // todo: will .value ever not be the string we inserted? like inserting weird chars or something 247 + if (this.$textarea.value != after) { 248 + this.$textarea.value = after 249 + good = false 250 + } 251 + this.run() 252 + return good 253 + } 254 + } 255 + Editor1.template = HTML` 256 + <div> 257 + <textarea-overlay $=overlay></textarea-overlay> 258 + <textarea $=textarea></textarea> 259 + </div> 260 + <style> 261 + :host { 262 + /* defaults */ 263 + white-space: pre-wrap; white-space: break-spaces; 264 + font-family: monospace; 265 + font-size: 1em; 266 + /* font-kerning: none; /* might be good to set this just in case? */ 267 + font-variant-ligatures: none; 268 + color: black; 269 + background: white; 270 + --inner-padding: 2px; /* use this instead of padding */ 271 + display: inline-block; 272 + margin: 1px; 273 + overflow-y: auto; 274 + border: 2px inset ThreeDLightShadow; 275 + scroll-padding: var(--inner-padding, 0px); 276 + } 277 + /* basically we have to ensure the textarea and overlay have all the same text styles */ 278 + /* either set to a fixed value, or inherited from the editor-container element */ 279 + :host > div, :host > div > * { 280 + all: initial; 281 + /* stuff that affects text positioning */ 282 + -webkit-text-size-adjust: none; 283 + white-space: inherit; 284 + word-break: break-word; 285 + font: inherit; 286 + font-size: 1em; 287 + tab-size: inherit; -moz-tab-size: inherit; 288 + letter-spacing: inherit; 289 + word-spacing: inherit; 290 + } 291 + :host > div { 292 + position: relative; 293 + contain: content; 294 + overflow: visible; 295 + display: block; 296 + min-height: 100%; /* fill remaining space, if host is sized */ 297 + } 298 + :host > div > * { 299 + display: block; 300 + overflow: hidden; overflow: clip; 301 + width: -webkit-fill-available; width: -moz-available; width: stretch; 302 + padding: var(--inner-padding, 0px); 303 + } 304 + :host > div > textarea { 305 + position: absolute; 306 + top: 0; 307 + z-index: 1; 308 + resize: none; 309 + height: 200%; /* todo: use like, grid or flex or something instead of */ 310 + overflow: hidden; 311 + 312 + background: transparent; 313 + -webkit-text-fill-color: transparent; /* sets color without affecting caret-color */ 314 + color: inherit; 315 + contain: strict; 316 + } 317 + :host > div > textarea-overlay { 318 + pointer-events: none; 319 + background: inherit; 320 + color: inherit; 321 + will-change: contents; 322 + contain: content; 323 + } 324 + /* necessary to avoid hiding last linebreak */ 325 + :host > div > textarea-overlay::after { 326 + content: "\\A"; 327 + } 328 + /* delegatesFocus is a newer feature, so we can't rely on :focus */ 329 + :host(:focus-within) { 330 + outline: auto; 331 + } 332 + </style> 333 + <style $=colors></style> 334 + ` 335 + Editor1.graph_categories = [ 336 + {color: 'gray', label: "input delay"}, 337 + {color: 'lime', label: "parsing"}, 338 + {color: '#F60', label: "rendering"}, 339 + {color: 'purple', label: "layout"}, 340 + ]
+3
editor1/site/export.js
··· 1 + export {default as HlTextarea} from './editor.js' 2 + export {default as EditorParser1} from './parse-example.js' 3 + export {finalize_facets} from './make-facets.js'
+147
editor1/site/facet-display.js
··· 1 + import {Widget, HTML} from './template.js' 2 + import {MARKUP_TYPE} from './make-facets.js' 3 + import {segmentize} from '@atcute/bluesky-richtext-segmenter' 4 + 5 + export class FacetDisplay extends Widget { 6 + constructor() { 7 + super() 8 + } 9 + show(post) { 10 + this.$list.textContent = "" 11 + function clean(text) { 12 + return text.replace(/\n/g, "⤶") 13 + } 14 + for (let {text, features=[]} of segmentize(post.text, post.facets)) { 15 + let span = FacetDisplay.templatespan() 16 + let lstrip = 0, rstrip = 0 17 + for (let f of features) { 18 + let d = FacetDisplay.templatefeature() 19 + if (f.strip) { 20 + lstrip = Math.max(f.strip[0],lstrip) 21 + rstrip = Math.max(f.strip[1],rstrip) 22 + } 23 + d.$root.append(JSON.stringify(f)) 24 + span.$data.append(d.$root) 25 + } 26 + rstrip = Math.min(rstrip, text.length-lstrip) 27 + span.$lstrip.textContent = clean(text.slice(0,lstrip)) 28 + span.$mid.textContent = clean(text.slice(lstrip, text.length-rstrip)) 29 + span.$rstrip.textContent = clean(text.slice(text.length-rstrip)) 30 + this.$list.append(span.$root) 31 + } 32 + this.$summary.textContent = post.text 33 + this.$length.textContent = post.grapheme_length 34 + } 35 + } 36 + FacetDisplay.template = HTML`<facet-display><div>Text (<span $=length></span>):</div><div class=summary $=summary></div>Segments:<div class=list $=list></div></facet-display>` 37 + FacetDisplay.templatespan = HTML`<div><div $=text><span $=lstrip class=strip></span><span $=mid></span><span $=rstrip class=strip></span></div><div $=data></div></div>` 38 + FacetDisplay.templatefeature = HTML`<div></div>` 39 + FacetDisplay.css = String.raw` 40 + facet-display { 41 + display: grid; 42 + grid-template-columns: max-content; 43 + } 44 + facet-display .list { 45 + display: grid; 46 + grid-template-columns: max-content max-content; 47 + grid-auto-rows: max-content; 48 + } 49 + facet-display .list > * { 50 + display: contents; 51 + } 52 + facet-display .summary, facet-display .list > * > :first-child { 53 + background: white; 54 + border: 1px solid; 55 + white-space: pre; 56 + font-family: monospace; 57 + padding: 0 1px; 58 + place-self: start; 59 + margin: 1px; 60 + place-content: center; 61 + } 62 + facet-display .list > * > :first-child { 63 + place-self: stretch end; 64 + display: flex; 65 + align-items: center; 66 + } 67 + facet-display .list > * > :last-child > *::before { 68 + content: "-"; 69 + } 70 + facet-display .list > * > :last-child { 71 + background: #eee; 72 + margin: 1px; 73 + } 74 + facet-display .strip { 75 + background: yellow; 76 + } 77 + ` 78 + 79 + export class PostDisplay extends Widget { 80 + constructor() { 81 + super() 82 + } 83 + show(post, {render_markup=true}={}) { 84 + this.$root.textContent = "" 85 + for (let {text, features=[]} of segmentize(post.text, post.facets)) { 86 + let core = document.createElement("br") // temp core 87 + let span = core 88 + let lstrip = 0, rstrip = 0 89 + function render_feature(feature, child) { 90 + if (feature.$type=="app.bsky.richtext.facet#link") { 91 + let elem = document.createElement('a') 92 + elem['href' /* sleeper agent activation phrase */ ] = feature.uri 93 + elem.append(child) 94 + return elem 95 + } 96 + if (feature.$type=="app.bsky.richtext.facet#tag") { 97 + let elem = document.createElement('a') 98 + elem['href'] = "about:blank" 99 + elem.append(child) 100 + return elem 101 + } 102 + if (feature.$type=="app.bsky.richtext.facet#mention") { 103 + let elem = document.createElement('a') 104 + elem['href'] = "about:blank" 105 + elem.append(child) 106 + return elem 107 + } 108 + if (feature.$type==MARKUP_TYPE && render_markup) { 109 + let styles = { 110 + italic: 'font-style:italic;', 111 + bold: 'font-weight:bold;', 112 + underline: 'text-decoration: underline;', 113 + strikethrough: 'text-decoration: line-through;', 114 + code: 'font-family:monospace, monospace; line-break: anywhere; background: #9995; padding: 0 0.1875em; border-radius: 4px; margin: 0 0.1em;', 115 + } 116 + if (styles[feature.style]) { 117 + lstrip = Math.max(feature.strip[0],lstrip) 118 + rstrip = Math.max(feature.strip[1],rstrip) 119 + let elem = document.createElement('span') 120 + elem.style = styles[feature.style] 121 + elem.append(child) 122 + return elem 123 + } 124 + } 125 + return child 126 + } 127 + for (let f of features) { 128 + span = render_feature(f, span) 129 + } 130 + rstrip = Math.min(rstrip, text.length-lstrip) 131 + this.$root.append(span) 132 + core.replaceWith(text.slice(lstrip, text.length-rstrip)) 133 + } 134 + } 135 + } 136 + PostDisplay.template = HTML`<bsky-post></bsky-post>` 137 + PostDisplay.css = String.raw` 138 + bsky-post { 139 + display: block; 140 + width: max-content; 141 + max-width: 500px; 142 + white-space: pre-wrap; 143 + border: solid; 144 + padding: 2px 4px; 145 + font-family: sans-serif; 146 + } 147 + `
+44
editor1/site/length-meter.js
··· 1 + import {Widget, HTML} from './template.js' 2 + 3 + export default class LengthMeter extends Widget { 4 + constructor() { 5 + super() 6 + } 7 + show(number, max) { 8 + this.$number.textContent = max - number 9 + this.$dial.style.setProperty('--color', number <= max ? "inherit" : "var(--over-color)") 10 + this.$dial.style.setProperty('--fill', `${number / max}turn`) 11 + } 12 + } 13 + LengthMeter.template = HTML` 14 + <length-meter> 15 + <div style='font-variant:tabular-nums' $=number></div> 16 + <div $=dial><div></div></div> 17 + </length-meter> 18 + ` 19 + LengthMeter.css = String.raw` 20 + length-meter { 21 + display: grid; 22 + grid-template-columns: 30px 20px; 23 + grid-template-rows: 20px; 24 + align-items: center; 25 + margin: 1px; 26 + font-size: 14px; 27 + white-space: pre; 28 + --color: #88F; 29 + --over-color: red; 30 + } 31 + length-meter > :nth-child(2) { 32 + border: 1px solid #c9c7c7; 33 + place-self: stretch; 34 + border-radius: 999px; 35 + background: padding-box conic-gradient(var(--color) 0turn var(--fill), white 0); 36 + display: grid; 37 + place-items: stretch; 38 + padding:3px; 39 + } 40 + length-meter > :nth-child(2) > div { 41 + border-radius: 999px; 42 + background: conic-gradient(var(--color) 0turn calc(var(--fill) - 1turn), white 0), white; 43 + } 44 + `
+215
editor1/site/lib/grapheme.js
··· 1 + // modified from unicode-segmenter license: MIT 2 + // https://github.com/cometkim/unicode-segmenter 3 + 4 + // grapheme parsing is one of the most intensive operations we do 5 + // so i really needed to optimize it to output less data 6 + 7 + // src/core.js 8 + function decodeUnicodeData(data, cats = "") { 9 + let buf = ( 10 + /** @type {Array<CategorizedUnicodeRange<T>>} */ 11 + [] 12 + ), nums = data.split(",").map((s) => s ? parseInt(s, 36) : 0), n = 0; 13 + for (let i = 0; i < nums.length; i++) 14 + i % 2 ? buf.push([ 15 + n, 16 + n + nums[i], 17 + /** @type {T} */ 18 + cats ? parseInt(cats[i >> 1], 36) : 0 19 + ]) : n = nums[i]; 20 + return buf; 21 + } 22 + function findUnicodeRangeIndex(cp, ranges, lo = 0, hi = ranges.length - 1) { 23 + while (lo <= hi) { 24 + let mid = lo + hi >>> 1, range = ranges[mid]; 25 + if (cp < range[0]) hi = mid - 1; 26 + else if (cp > range[1]) lo = mid + 1; 27 + else return mid; 28 + } 29 + return -1; 30 + } 31 + 32 + // src/_grapheme_data.js 33 + var GraphemeCategory = { 34 + Any: 0, 35 + CR: 1, 36 + Control: 2, 37 + Extend: 3, 38 + Extended_Pictographic: 4, 39 + L: 5, 40 + LF: 6, 41 + LV: 7, 42 + LVT: 8, 43 + Prepend: 9, 44 + Regional_Indicator: 10, 45 + SpacingMark: 11, 46 + T: 12, 47 + V: 13, 48 + ZWJ: 14 49 + }; 50 + var grapheme_ranges = decodeUnicodeData( 51 + /** @type {UnicodeDataEncoding} */ 52 + ",9,a,,b,1,d,,e,h,3j,w,4p,,4t,,4u,,lc,33,w3,6,13l,18,14v,,14x,1,150,1,153,,16o,5,174,a,17g,,18r,k,19s,,1cm,6,1ct,,1cv,5,1d3,1,1d6,3,1e7,,1e9,,1f4,q,1ie,a,1kb,8,1kt,,1li,3,1ln,8,1lx,2,1m1,4,1nd,2,1ow,1,1p3,8,1qi,n,1r6,,1r7,v,1s3,,1tm,,1tn,,1to,,1tq,2,1tt,7,1u1,3,1u5,,1u6,1,1u9,6,1uq,1,1vl,,1vm,1,1x8,,1xa,,1xb,1,1xd,3,1xj,1,1xn,1,1xp,,1xz,,1ya,1,1z2,,1z5,1,1z7,,20s,,20u,2,20x,1,213,1,217,2,21d,,228,1,22d,,22p,1,22r,,24c,,24e,2,24h,4,24n,1,24p,,24r,1,24t,,25e,1,262,5,269,,26a,1,27w,,27y,1,280,,281,3,287,1,28b,1,28d,,28l,2,28y,1,29u,,2bi,,2bj,,2bk,,2bl,1,2bq,2,2bu,2,2bx,,2c7,,2dc,,2dd,2,2dg,,2f0,,2f2,2,2f5,3,2fa,2,2fe,3,2fp,1,2g2,1,2gx,,2gy,1,2ik,,2im,,2in,1,2ip,,2iq,,2ir,1,2iu,2,2iy,3,2j9,1,2jm,1,2k3,,2kg,1,2ki,1,2m3,1,2m6,,2m7,1,2m9,3,2me,2,2mi,2,2ml,,2mm,,2mv,,2n6,1,2o1,,2o2,1,2q2,,2q7,,2q8,1,2qa,2,2qe,,2qg,6,2qn,,2r6,1,2sx,,2sz,,2t0,6,2tj,7,2wh,,2wj,,2wk,8,2x4,6,2zc,1,305,,307,,309,,30e,1,31t,d,327,,328,4,32e,1,32l,a,32x,z,346,,371,3,375,,376,5,37d,1,37f,1,37h,1,386,1,388,1,38e,2,38x,3,39e,,39g,,39h,1,39p,,3a5,,3cw,2n,3fk,1z,3hk,2f,3tp,2,4k2,3,4ky,2,4lu,1,4mq,1,4ok,1,4om,,4on,6,4ou,7,4p2,,4p3,1,4p5,a,4pp,,4qz,2,4r2,,4r3,,4ud,1,4vd,,4yo,2,4yr,3,4yv,1,4yx,2,4z4,1,4z6,,4z7,5,4zd,2,55j,1,55l,1,55n,,579,,57a,,57b,,57c,6,57k,,57m,,57p,7,57x,5,583,9,58f,,59s,19,5b4,b,5c0,3,5c4,,5dg,9,5dq,3,5du,2,5ez,8,5fk,1,5fm,,5gh,,5gi,3,5gm,1,5go,5,5ie,,5if,,5ig,1,5ii,2,5il,,5im,,5in,4,5k4,7,5kc,7,5kk,1,5km,1,5ow,2,5p0,c,5pd,,5pe,6,5pp,,5pw,,5pz,,5q0,1,5vk,1r,6bv,,6bw,,6bx,,6by,1,6co,6,6d8,,6dl,,6e8,f,6hc,w,6jm,,6k9,,6ms,5,6nd,1,6xm,1,6y0,,72n,,73d,a,73s,2,79e,,7fu,1,7g6,,7gg,,7i3,3,7i8,4,7im,,7ip,,7is,1,7iw,,7j1,,7j4,,7j6,1,7ja,,7je,,7ji,1,7js,2,7k0,,7k2,,7k8,b,7kv,1,7kz,,7l1,1,7l4,,7ln,,7lq,1,7ma,5,7mh,,7mj,1,7mo,1,7mv,,7my,1,7n4,1,7nh,1,7no,1,7ns,,7ny,1,7o1,,7o3,1,7op,1,7ow,5,7p3,3,7p9,,7pe,,7ph,,7pk,5,7pr,,7pu,,7pw,,7py,,7q5,,7q9,,7qg,,7qr,1,7r8,,7rb,,7rg,,7ri,,7rn,2,7rr,,7s3,1,7th,2,7tt,,7u8,,7un,,850,1,8hx,2,8ij,1,8k0,,8k5,,8vj,2,8zj,,928,v,wvj,3,wvo,9,wwu,1,wz4,1,x6q,,x6u,,x6z,,x7n,1,x7p,1,x7r,,x7w,,xa8,1,xbo,f,xc4,1,xcw,h,xdr,,xeu,7,xfr,a,xg2,,xg3,,xgg,s,xhc,2,xhf,,xir,,xis,1,xiu,3,xiy,1,xj0,1,xj2,1,xj4,,xk5,,xm1,5,xm7,1,xm9,1,xmb,1,xmd,1,xmr,,xn0,,xn1,,xoc,,xps,,xpu,2,xpz,1,xq6,1,xq9,,xrf,,xrg,1,xri,1,xrp,,xrq,,xyb,1,xyd,,xye,1,xyg,,xyh,1,xyk,,xyl,,1e68,f,1e74,f,1edb,,1ehq,1,1ek0,b,1eyl,,1f4w,,1f92,4,1gjl,2,1gjp,1,1gjw,3,1gl4,2,1glb,,1gpx,1,1h5w,3,1h7t,4,1hgr,1,1hiy,5,1hl2,a,1hmq,3,1hq8,,1hq9,,1hqa,,1hrs,e,1htc,,1htf,1,1htr,2,1htu,,1hv4,2,1hv7,3,1hvb,1,1hvd,1,1hvh,,1hvm,,1hvx,,1hxc,2,1hyf,4,1hyk,,1hyl,7,1hz9,1,1i0j,,1i0w,1,1i0y,,1i2b,2,1i2e,8,1i2n,,1i2o,,1i2q,1,1i2x,3,1i32,,1i33,,1i5o,2,1i5r,2,1i5u,1,1i5w,3,1i66,,1i69,,1ian,,1iao,2,1iar,7,1ibk,1,1ibm,1,1id7,1,1ida,,1idb,,1idc,,1idd,3,1idj,1,1idn,1,1idp,,1idz,,1iea,1,1iee,6,1ieo,4,1igo,,1igp,1,1igr,5,1igy,,1ih1,,1ih3,2,1ih6,,1ih8,1,1iha,2,1ihd,,1ihe,,1iht,1,1ik5,2,1ik8,7,1ikg,1,1iki,2,1ikl,,1ikm,,1ila,,1ink,,1inl,1,1inn,5,1int,,1inu,,1inv,1,1inx,,1iny,,1inz,1,1io1,,1io2,1,1iun,,1iuo,1,1iuq,3,1iuw,3,1iv0,1,1iv2,,1iv3,1,1ivw,1,1iy8,2,1iyb,7,1iyj,1,1iyl,,1iym,,1iyn,1,1j1n,,1j1o,,1j1p,,1j1q,1,1j1s,7,1j4t,,1j4u,,1j4v,,1j4y,3,1j52,,1j53,4,1jcc,2,1jcf,8,1jco,,1jcp,1,1jjk,,1jjl,4,1jjr,1,1jjv,3,1jjz,,1jk0,,1jk1,,1jk2,,1jk3,,1jo1,2,1jo4,3,1joa,1,1joc,3,1jog,,1jok,,1jpd,9,1jqr,5,1jqx,,1jqz,3,1jrb,,1jrl,5,1jrr,1,1jrt,2,1jt0,5,1jt6,c,1jtj,,1jtk,1,1jz4,,1jz5,,1jz6,2,1jz9,,1jza,,1jzb,,1k4v,,1k4w,6,1k54,5,1k5a,,1k5b,,1k7m,l,1k89,,1k8a,6,1k8h,,1k8i,1,1k8k,,1k8l,1,1kc1,5,1kca,,1kcc,1,1kcf,6,1kcm,,1kcn,,1kei,4,1keo,1,1ker,1,1ket,,1keu,,1kev,,1koj,1,1kol,1,1kow,1,1koy,,1koz,,1kqc,1,1kqe,4,1kqm,1,1kqo,2,1kre,,1ovk,f,1ow0,,1ow7,e,1xr2,b,1xre,2,1xrh,2,1zow,4,1zqo,6,206b,,206f,3,20jz,,20k1,1i,20lr,3,20o4,,20og,1,2ftp,1,2fts,3,2jgg,19,2jhs,m,2jxh,4,2jxp,5,2jxv,7,2jy3,7,2jyd,6,2jze,3,2k3m,2,2lmo,1i,2lob,1d,2lpx,,2lqc,,2lqz,4,2lr5,e,2mtc,6,2mtk,g,2mu3,6,2mub,1,2mue,4,2mxb,,2n1s,6,2nce,,2ne4,3,2nsc,3,2nzi,1,2o6b,,2o6e,,2o6m,1,2o6t,,2ok0,6,2on8,6,2pz8,,2q0c,3,2q38,b,2q3z,1,2q4g,,2q4v,1,2q5y,9,2q9c,1,2q9q,1,2qa6,,2qa9,9,2qb2,1j,2qcm,p,2qdd,e,2qe2,,2qen,,2qeq,8,2qf0,3,2qfd,m,2qg6,57,2qlg,33,2qom,1,2qop,2,2qou,2a,2qr7,2,2qrb,3,2qrf,4,2qrk,71,2qyn,1q,2r0p,5,2r0w,n,2r1r,1,2r1v,7,2r2f,,2r2i,3,2r2o,,2r2t,1,2r38,1,2r3c,,2r3l,1,2r3w,,2r42,2,2r4h,2,2r4s,2,2r4x,,2r4z,,2r54,,2r5b,,2r5f,,2r5m,2d,2r9c,1x,2rbf,7,2rbp,g,2rc9,,2rcb,5,2rcj,c,2riy,11,2rkc,3,2rm0,7,2rmi,5,2rns,7,2rou,1,2rp8,3,2rpe,d,2rq1,12,2rrg,1a,2rss,9,2rt3,54,2s0o,7,2s1a,41,2scg,sd,jny8,v,jnz4,2n,jo1s,3j,jo5c,6n,joc0,2rz", 53 + "262122424333333393233393339333333333393393b3b3b3b3b333b33b3bb33333b3b3333333b3b33bb3333b33b3bb33333b3bbb333b333b33333b3b3b3b3333b3b33b3bb39333b33b33b3b3b333b333333b3b333333b33b3b3333b3335dc333333b3b3b33323333b3bb3b33b3b3b3333b33333b3b333bb3b33b3b3b3b3b333b333b3323e22442344444444444444444444444444444444444444444444444444444444444444444444444444444443333333333b3b3bb33333b353b3b3b3b333b3b333b333333b3bb3b3b3bb333232333333333333333b3b3333bb3b393933b3b33bb3b393b3b3b3333b33b33b3bbb33b333b3333bb3933b3b3b333b3b3b3b3b33b3b3b33b3b3b33b3b33b33b3b3b33bb39b9b3b33b3b33b333b393b33b3b3bb33b33b3b3b3333393b3b3b33b39bb3b332333b333dd3b3333233332333333333333333333333333333444444444444a444444444444434444444444444444444444444444444444444444444423232" 54 + ); 55 + 56 + // src/_incb_data.js 57 + var consonant_ranges = decodeUnicodeData( 58 + /** @type {UnicodeDataEncoding} */ 59 + "1sl,10,1ug,7,1vc,7,1w5,j,1wq,6,1wy,,1x2,3,1y4,1,1y7,,1yo,1,239,j,23u,6,242,1,245,4,261,,26t,j,27e,6,27m,1,27p,4,28s,1,28v,,29d,,2dx,j,2ei,f,2fs,2,2l1,11,35s,16,37j,,380,5,38a,3,38h,,38l,1,38u,2,391,c,39q,,4n4,1f,55s,1g,5cb,1,5cj,w,5dx,7,5fn,t,5gu,1,5h7,2,xhl,2,xhr,z,xk0,4,xk7,8,xkq,4,xnk,f,xo1,2,xoa,,xoe,1,xr4,a,xxc,q,1gjk,,1gk0,3,1gk5,2,1gk9,s,1hxf,z,1hz8,,1hzb,,1if4,9,1iff,,1ifi,,1ifk,11,1ji8,6,1jih,,1jik,7,1jit,1,1jiw,n,1jpc,,1jpn,13,1jrk,,1jrw,13,1kp0,c,1kpe,x" 60 + ); 61 + 62 + // src/grapheme.js 63 + var BMP_MAX = 65535; 64 + export function grapheme_cost(input, grapheme_limit) { 65 + let count = 0 66 + let limit = Infinity 67 + let cp = input.codePointAt(0); 68 + if (cp == null) return {over_index: limit, cost: count}; 69 + let cursor = cp <= BMP_MAX ? 1 : 2; 70 + let len = input.length; 71 + let catBefore = cat(cp); 72 + let catAfter = 0; 73 + let risCount = 0; 74 + let emoji = false; 75 + let consonant = false; 76 + let linker = false; 77 + let index = 0; 78 + let _catBegin = catBefore; 79 + let _hd = cp; 80 + while (cursor < len) { 81 + cp = /** @type {number} */ 82 + input.codePointAt(cursor); 83 + catAfter = cat(cp); 84 + let boundary = true; 85 + if (catBefore === 1) { 86 + boundary = catAfter !== 6; 87 + } else if (catBefore === 2 || catBefore === 6) { 88 + boundary = true; 89 + } else if (catAfter === 1 || catAfter === 2 || catAfter === 6) { 90 + boundary = true; 91 + } else if (catAfter === 3 || catAfter === 14 || catAfter === 11) { 92 + boundary = false; 93 + } else if (catBefore === 9) { 94 + boundary = false; 95 + } else if (catBefore === 14 && catAfter === 4) { 96 + boundary = !emoji; 97 + } else if (catBefore === 10 && catAfter === 10) { 98 + boundary = risCount++ % 2 === 1; 99 + } else if (catBefore === 5) { 100 + boundary = !(catAfter === 5 || catAfter === 13 || catAfter === 7 || catAfter === 8); 101 + } else if ((catBefore === 7 || catBefore === 13) && (catAfter === 13 || catAfter === 12)) { 102 + boundary = false; 103 + } else if ((catBefore === 8 || catBefore === 12) && catAfter === 12) { 104 + boundary = false; 105 + } else if (catAfter === 0 && consonant && linker && isIndicConjunctConsonant(cp)) { 106 + boundary = false; 107 + } 108 + if (boundary) { 109 + count++ 110 + if (Infinity==limit && count > grapheme_limit) { 111 + limit = index 112 + } 113 + emoji = false; 114 + risCount = 0; 115 + index = cursor; 116 + _catBegin = catAfter; 117 + _hd = cp; 118 + } else { 119 + if (catAfter === 14 && (catBefore === 3 || catBefore === 4)) { 120 + emoji = true; 121 + } else if (cp >= 2325) { 122 + if (!consonant && catBefore === 0) { 123 + consonant = isIndicConjunctConsonant(_hd); 124 + } 125 + if (consonant && catAfter === 3) { 126 + linker = linker || cp === 2381 || cp === 2509 || cp === 2637 || cp === 2765 || cp === 2893 || cp === 3149 || cp === 3405 || cp === 4153 || cp === 6098 || cp === 6752 || cp === 6980 || cp === 7083 || cp === 43456 || cp === 43766 || cp === 68159 || cp === 69939 || cp === 70608 || cp === 71998 || cp === 72263 || cp === 72345 || cp === 73538; 127 + } else { 128 + linker = false; 129 + } 130 + } 131 + } 132 + cursor += cp <= BMP_MAX ? 1 : 2; 133 + catBefore = catAfter; 134 + } 135 + if (index < len) { 136 + count++ 137 + if (Infinity==limit && count > grapheme_limit) { 138 + limit = index 139 + } 140 + } 141 + return {over_index: limit, cost: count} 142 + } 143 + var SEG0 = new Uint8Array(6080); 144 + var SEG0_MIN = 128; 145 + var SEG0_MAX = 12287; 146 + var SEG1 = new Uint8Array(1536); 147 + var SEG1_MIN = 40960; 148 + var SEG1_MAX = 44031; 149 + var SEG_CURSOR = (() => { 150 + let cursor = 0; 151 + while (true) { 152 + let [start, end, cat2] = grapheme_ranges[cursor]; 153 + if (start > SEG1_MAX) break; 154 + cursor++; 155 + if (end < SEG0_MIN || start > SEG0_MAX && end < SEG1_MIN) continue; 156 + for (let cp = start; cp <= end; cp++) { 157 + let seg, idx = 0; 158 + if (cp <= SEG0_MAX) { 159 + seg = SEG0; 160 + idx = cp - SEG0_MIN >> 1; 161 + } else { 162 + seg = SEG1; 163 + idx = cp - SEG1_MIN >> 1; 164 + } 165 + seg[idx] = cp & 1 ? seg[idx] & 15 | cat2 << 4 : seg[idx] & 240 | cat2; 166 + } 167 + } 168 + return cursor; 169 + })(); 170 + function cat(cp) { 171 + if (cp < SEG0_MIN) { 172 + if (cp >= 32) return 0; 173 + if (cp === 10) return 6; 174 + if (cp === 13) return 1; 175 + return 2; 176 + } 177 + if (cp <= SEG0_MAX) { 178 + let byte = SEG0[cp - SEG0_MIN >> 1]; 179 + return ( 180 + /** @type {GraphemeCategoryNum} */ 181 + cp & 1 ? byte >> 4 : byte & 15 182 + ); 183 + } 184 + if (cp < SEG1_MIN) { 185 + if (cp < 12336) return cp >= 12330 ? 3 : 0; 186 + if (cp < 12443) { 187 + if (cp === 12336 || cp === 12349) return 4; 188 + return cp >= 12441 ? 3 : 0; 189 + } 190 + if (cp === 12951 || cp === 12953) return 4; 191 + return 0; 192 + } 193 + if (cp <= SEG1_MAX) { 194 + let byte = SEG1[cp - SEG1_MIN >> 1]; 195 + return ( 196 + /** @type {GraphemeCategoryNum} */ 197 + cp & 1 ? byte >> 4 : byte & 15 198 + ); 199 + } 200 + if (cp <= 55203) { 201 + return (cp - 44032) % 28 === 0 ? 7 : 8; 202 + } 203 + if (cp <= 55295) { 204 + if (cp <= 55238) return cp >= 55216 ? 13 : 0; 205 + return cp >= 55243 ? 12 : 0; 206 + } 207 + if (cp < 65024) { 208 + return cp === 64286 ? 3 : 0; 209 + } 210 + let idx = findUnicodeRangeIndex(cp, grapheme_ranges, SEG_CURSOR); 211 + return idx < 0 ? 0 : grapheme_ranges[idx][2]; 212 + } 213 + function isIndicConjunctConsonant(cp) { 214 + return findUnicodeRangeIndex(cp, consonant_ranges) >= 0; 215 + }
+2
editor1/site/lib/grapheme2.js
··· 1 + GRAPHEME_REGEX = /(?:\r\n|[\u0000-\u0009\u000B-\u000C\u000E-\u001F\u007F-\u009F\u00AD\u061C\u180E\u200B\u200E-\u200F\u2028\u2029\u202A-\u202E\u2060-\u2064\u2065\u2066-\u206F\uFEFF\uFFF0-\uFFF8\uFFF9-\uFFFB\u{13430}-\u{1343F}\u{1BCA0}-\u{1BCA3}\u{1D173}-\u{1D17A}\u{E0000}\u{E0001}\u{E0002}-\u{E001F}\u{E0080}-\u{E00FF}\u{E01F0}-\u{E0FFF}\r\n]|[\u0600-\u0605\u06DD\u070F\u0890-\u0891\u08E2\u0D4E\u{110BD}\u{110CD}\u{111C2}-\u{111C3}\u{113D1}\u{1193F}\u{11941}\u{11A84}-\u{11A89}\u{11D46}\u{11F02}]*(?:[\u1100-\u115F\uA960-\uA97C]*(?:[\uAC00\uAC1C\uAC38\uAC54\uAC70\uAC8C\uACA8\uACC4\uACE0\uACFC\uAD18\uAD34\uAD50\uAD6C\uAD88\uADA4\uADC0\uADDC\uADF8\uAE14\uAE30\uAE4C\uAE68\uAE84\uAEA0\uAEBC\uAED8\uAEF4\uAF10\uAF2C\uAF48\uAF64\uAF80\uAF9C\uAFB8\uAFD4\uAFF0\uB00C\uB028\uB044\uB060\uB07C\uB098\uB0B4\uB0D0\uB0EC\uB108\uB124\uB140\uB15C\uB178\uB194\uB1B0\uB1CC\uB1E8\uB204\uB220\uB23C\uB258\uB274\uB290\uB2AC\uB2C8\uB2E4\uB300\uB31C\uB338\uB354\uB370\uB38C\uB3A8\uB3C4\uB3E0\uB3FC\uB418\uB434\uB450\uB46C\uB488\uB4A4\uB4C0\uB4DC\uB4F8\uB514\uB530\uB54C\uB568\uB584\uB5A0\uB5BC\uB5D8\uB5F4\uB610\uB62C\uB648\uB664\uB680\uB69C\uB6B8\uB6D4\uB6F0\uB70C\uB728\uB744\uB760\uB77C\uB798\uB7B4\uB7D0\uB7EC\uB808\uB824\uB840\uB85C\uB878\uB894\uB8B0\uB8CC\uB8E8\uB904\uB920\uB93C\uB958\uB974\uB990\uB9AC\uB9C8\uB9E4\uBA00\uBA1C\uBA38\uBA54\uBA70\uBA8C\uBAA8\uBAC4\uBAE0\uBAFC\uBB18\uBB34\uBB50\uBB6C\uBB88\uBBA4\uBBC0\uBBDC\uBBF8\uBC14\uBC30\uBC4C\uBC68\uBC84\uBCA0\uBCBC\uBCD8\uBCF4\uBD10\uBD2C\uBD48\uBD64\uBD80\uBD9C\uBDB8\uBDD4\uBDF0\uBE0C\uBE28\uBE44\uBE60\uBE7C\uBE98\uBEB4\uBED0\uBEEC\uBF08\uBF24\uBF40\uBF5C\uBF78\uBF94\uBFB0\uBFCC\uBFE8\uC004\uC020\uC03C\uC058\uC074\uC090\uC0AC\uC0C8\uC0E4\uC100\uC11C\uC138\uC154\uC170\uC18C\uC1A8\uC1C4\uC1E0\uC1FC\uC218\uC234\uC250\uC26C\uC288\uC2A4\uC2C0\uC2DC\uC2F8\uC314\uC330\uC34C\uC368\uC384\uC3A0\uC3BC\uC3D8\uC3F4\uC410\uC42C\uC448\uC464\uC480\uC49C\uC4B8\uC4D4\uC4F0\uC50C\uC528\uC544\uC560\uC57C\uC598\uC5B4\uC5D0\uC5EC\uC608\uC624\uC640\uC65C\uC678\uC694\uC6B0\uC6CC\uC6E8\uC704\uC720\uC73C\uC758\uC774\uC790\uC7AC\uC7C8\uC7E4\uC800\uC81C\uC838\uC854\uC870\uC88C\uC8A8\uC8C4\uC8E0\uC8FC\uC918\uC934\uC950\uC96C\uC988\uC9A4\uC9C0\uC9DC\uC9F8\uCA14\uCA30\uCA4C\uCA68\uCA84\uCAA0\uCABC\uCAD8\uCAF4\uCB10\uCB2C\uCB48\uCB64\uCB80\uCB9C\uCBB8\uCBD4\uCBF0\uCC0C\uCC28\uCC44\uCC60\uCC7C\uCC98\uCCB4\uCCD0\uCCEC\uCD08\uCD24\uCD40\uCD5C\uCD78\uCD94\uCDB0\uCDCC\uCDE8\uCE04\uCE20\uCE3C\uCE58\uCE74\uCE90\uCEAC\uCEC8\uCEE4\uCF00\uCF1C\uCF38\uCF54\uCF70\uCF8C\uCFA8\uCFC4\uCFE0\uCFFC\uD018\uD034\uD050\uD06C\uD088\uD0A4\uD0C0\uD0DC\uD0F8\uD114\uD130\uD14C\uD168\uD184\uD1A0\uD1BC\uD1D8\uD1F4\uD210\uD22C\uD248\uD264\uD280\uD29C\uD2B8\uD2D4\uD2F0\uD30C\uD328\uD344\uD360\uD37C\uD398\uD3B4\uD3D0\uD3EC\uD408\uD424\uD440\uD45C\uD478\uD494\uD4B0\uD4CC\uD4E8\uD504\uD520\uD53C\uD558\uD574\uD590\uD5AC\uD5C8\uD5E4\uD600\uD61C\uD638\uD654\uD670\uD68C\uD6A8\uD6C4\uD6E0\uD6FC\uD718\uD734\uD750\uD76C\uD788][\u1160-\u11A7\uD7B0-\uD7C6\u{16D63}\u{16D67}-\u{16D6A}]*|[\u1160-\u11A7\uD7B0-\uD7C6\u{16D63}\u{16D67}-\u{16D6A}]+)[\u11A8-\u11FF\uD7CB-\uD7FB]*|[\u1100-\u115F\uA960-\uA97C]*[\uAC01-\uAC1B\uAC1D-\uAC37\uAC39-\uAC53\uAC55-\uAC6F\uAC71-\uAC8B\uAC8D-\uACA7\uACA9-\uACC3\uACC5-\uACDF\uACE1-\uACFB\uACFD-\uAD17\uAD19-\uAD33\uAD35-\uAD4F\uAD51-\uAD6B\uAD6D-\uAD87\uAD89-\uADA3\uADA5-\uADBF\uADC1-\uADDB\uADDD-\uADF7\uADF9-\uAE13\uAE15-\uAE2F\uAE31-\uAE4B\uAE4D-\uAE67\uAE69-\uAE83\uAE85-\uAE9F\uAEA1-\uAEBB\uAEBD-\uAED7\uAED9-\uAEF3\uAEF5-\uAF0F\uAF11-\uAF2B\uAF2D-\uAF47\uAF49-\uAF63\uAF65-\uAF7F\uAF81-\uAF9B\uAF9D-\uAFB7\uAFB9-\uAFD3\uAFD5-\uAFEF\uAFF1-\uB00B\uB00D-\uB027\uB029-\uB043\uB045-\uB05F\uB061-\uB07B\uB07D-\uB097\uB099-\uB0B3\uB0B5-\uB0CF\uB0D1-\uB0EB\uB0ED-\uB107\uB109-\uB123\uB125-\uB13F\uB141-\uB15B\uB15D-\uB177\uB179-\uB193\uB195-\uB1AF\uB1B1-\uB1CB\uB1CD-\uB1E7\uB1E9-\uB203\uB205-\uB21F\uB221-\uB23B\uB23D-\uB257\uB259-\uB273\uB275-\uB28F\uB291-\uB2AB\uB2AD-\uB2C7\uB2C9-\uB2E3\uB2E5-\uB2FF\uB301-\uB31B\uB31D-\uB337\uB339-\uB353\uB355-\uB36F\uB371-\uB38B\uB38D-\uB3A7\uB3A9-\uB3C3\uB3C5-\uB3DF\uB3E1-\uB3FB\uB3FD-\uB417\uB419-\uB433\uB435-\uB44F\uB451-\uB46B\uB46D-\uB487\uB489-\uB4A3\uB4A5-\uB4BF\uB4C1-\uB4DB\uB4DD-\uB4F7\uB4F9-\uB513\uB515-\uB52F\uB531-\uB54B\uB54D-\uB567\uB569-\uB583\uB585-\uB59F\uB5A1-\uB5BB\uB5BD-\uB5D7\uB5D9-\uB5F3\uB5F5-\uB60F\uB611-\uB62B\uB62D-\uB647\uB649-\uB663\uB665-\uB67F\uB681-\uB69B\uB69D-\uB6B7\uB6B9-\uB6D3\uB6D5-\uB6EF\uB6F1-\uB70B\uB70D-\uB727\uB729-\uB743\uB745-\uB75F\uB761-\uB77B\uB77D-\uB797\uB799-\uB7B3\uB7B5-\uB7CF\uB7D1-\uB7EB\uB7ED-\uB807\uB809-\uB823\uB825-\uB83F\uB841-\uB85B\uB85D-\uB877\uB879-\uB893\uB895-\uB8AF\uB8B1-\uB8CB\uB8CD-\uB8E7\uB8E9-\uB903\uB905-\uB91F\uB921-\uB93B\uB93D-\uB957\uB959-\uB973\uB975-\uB98F\uB991-\uB9AB\uB9AD-\uB9C7\uB9C9-\uB9E3\uB9E5-\uB9FF\uBA01-\uBA1B\uBA1D-\uBA37\uBA39-\uBA53\uBA55-\uBA6F\uBA71-\uBA8B\uBA8D-\uBAA7\uBAA9-\uBAC3\uBAC5-\uBADF\uBAE1-\uBAFB\uBAFD-\uBB17\uBB19-\uBB33\uBB35-\uBB4F\uBB51-\uBB6B\uBB6D-\uBB87\uBB89-\uBBA3\uBBA5-\uBBBF\uBBC1-\uBBDB\uBBDD-\uBBF7\uBBF9-\uBC13\uBC15-\uBC2F\uBC31-\uBC4B\uBC4D-\uBC67\uBC69-\uBC83\uBC85-\uBC9F\uBCA1-\uBCBB\uBCBD-\uBCD7\uBCD9-\uBCF3\uBCF5-\uBD0F\uBD11-\uBD2B\uBD2D-\uBD47\uBD49-\uBD63\uBD65-\uBD7F\uBD81-\uBD9B\uBD9D-\uBDB7\uBDB9-\uBDD3\uBDD5-\uBDEF\uBDF1-\uBE0B\uBE0D-\uBE27\uBE29-\uBE43\uBE45-\uBE5F\uBE61-\uBE7B\uBE7D-\uBE97\uBE99-\uBEB3\uBEB5-\uBECF\uBED1-\uBEEB\uBEED-\uBF07\uBF09-\uBF23\uBF25-\uBF3F\uBF41-\uBF5B\uBF5D-\uBF77\uBF79-\uBF93\uBF95-\uBFAF\uBFB1-\uBFCB\uBFCD-\uBFE7\uBFE9-\uC003\uC005-\uC01F\uC021-\uC03B\uC03D-\uC057\uC059-\uC073\uC075-\uC08F\uC091-\uC0AB\uC0AD-\uC0C7\uC0C9-\uC0E3\uC0E5-\uC0FF\uC101-\uC11B\uC11D-\uC137\uC139-\uC153\uC155-\uC16F\uC171-\uC18B\uC18D-\uC1A7\uC1A9-\uC1C3\uC1C5-\uC1DF\uC1E1-\uC1FB\uC1FD-\uC217\uC219-\uC233\uC235-\uC24F\uC251-\uC26B\uC26D-\uC287\uC289-\uC2A3\uC2A5-\uC2BF\uC2C1-\uC2DB\uC2DD-\uC2F7\uC2F9-\uC313\uC315-\uC32F\uC331-\uC34B\uC34D-\uC367\uC369-\uC383\uC385-\uC39F\uC3A1-\uC3BB\uC3BD-\uC3D7\uC3D9-\uC3F3\uC3F5-\uC40F\uC411-\uC42B\uC42D-\uC447\uC449-\uC463\uC465-\uC47F\uC481-\uC49B\uC49D-\uC4B7\uC4B9-\uC4D3\uC4D5-\uC4EF\uC4F1-\uC50B\uC50D-\uC527\uC529-\uC543\uC545-\uC55F\uC561-\uC57B\uC57D-\uC597\uC599-\uC5B3\uC5B5-\uC5CF\uC5D1-\uC5EB\uC5ED-\uC607\uC609-\uC623\uC625-\uC63F\uC641-\uC65B\uC65D-\uC677\uC679-\uC693\uC695-\uC6AF\uC6B1-\uC6CB\uC6CD-\uC6E7\uC6E9-\uC703\uC705-\uC71F\uC721-\uC73B\uC73D-\uC757\uC759-\uC773\uC775-\uC78F\uC791-\uC7AB\uC7AD-\uC7C7\uC7C9-\uC7E3\uC7E5-\uC7FF\uC801-\uC81B\uC81D-\uC837\uC839-\uC853\uC855-\uC86F\uC871-\uC88B\uC88D-\uC8A7\uC8A9-\uC8C3\uC8C5-\uC8DF\uC8E1-\uC8FB\uC8FD-\uC917\uC919-\uC933\uC935-\uC94F\uC951-\uC96B\uC96D-\uC987\uC989-\uC9A3\uC9A5-\uC9BF\uC9C1-\uC9DB\uC9DD-\uC9F7\uC9F9-\uCA13\uCA15-\uCA2F\uCA31-\uCA4B\uCA4D-\uCA67\uCA69-\uCA83\uCA85-\uCA9F\uCAA1-\uCABB\uCABD-\uCAD7\uCAD9-\uCAF3\uCAF5-\uCB0F\uCB11-\uCB2B\uCB2D-\uCB47\uCB49-\uCB63\uCB65-\uCB7F\uCB81-\uCB9B\uCB9D-\uCBB7\uCBB9-\uCBD3\uCBD5-\uCBEF\uCBF1-\uCC0B\uCC0D-\uCC27\uCC29-\uCC43\uCC45-\uCC5F\uCC61-\uCC7B\uCC7D-\uCC97\uCC99-\uCCB3\uCCB5-\uCCCF\uCCD1-\uCCEB\uCCED-\uCD07\uCD09-\uCD23\uCD25-\uCD3F\uCD41-\uCD5B\uCD5D-\uCD77\uCD79-\uCD93\uCD95-\uCDAF\uCDB1-\uCDCB\uCDCD-\uCDE7\uCDE9-\uCE03\uCE05-\uCE1F\uCE21-\uCE3B\uCE3D-\uCE57\uCE59-\uCE73\uCE75-\uCE8F\uCE91-\uCEAB\uCEAD-\uCEC7\uCEC9-\uCEE3\uCEE5-\uCEFF\uCF01-\uCF1B\uCF1D-\uCF37\uCF39-\uCF53\uCF55-\uCF6F\uCF71-\uCF8B\uCF8D-\uCFA7\uCFA9-\uCFC3\uCFC5-\uCFDF\uCFE1-\uCFFB\uCFFD-\uD017\uD019-\uD033\uD035-\uD04F\uD051-\uD06B\uD06D-\uD087\uD089-\uD0A3\uD0A5-\uD0BF\uD0C1-\uD0DB\uD0DD-\uD0F7\uD0F9-\uD113\uD115-\uD12F\uD131-\uD14B\uD14D-\uD167\uD169-\uD183\uD185-\uD19F\uD1A1-\uD1BB\uD1BD-\uD1D7\uD1D9-\uD1F3\uD1F5-\uD20F\uD211-\uD22B\uD22D-\uD247\uD249-\uD263\uD265-\uD27F\uD281-\uD29B\uD29D-\uD2B7\uD2B9-\uD2D3\uD2D5-\uD2EF\uD2F1-\uD30B\uD30D-\uD327\uD329-\uD343\uD345-\uD35F\uD361-\uD37B\uD37D-\uD397\uD399-\uD3B3\uD3B5-\uD3CF\uD3D1-\uD3EB\uD3ED-\uD407\uD409-\uD423\uD425-\uD43F\uD441-\uD45B\uD45D-\uD477\uD479-\uD493\uD495-\uD4AF\uD4B1-\uD4CB\uD4CD-\uD4E7\uD4E9-\uD503\uD505-\uD51F\uD521-\uD53B\uD53D-\uD557\uD559-\uD573\uD575-\uD58F\uD591-\uD5AB\uD5AD-\uD5C7\uD5C9-\uD5E3\uD5E5-\uD5FF\uD601-\uD61B\uD61D-\uD637\uD639-\uD653\uD655-\uD66F\uD671-\uD68B\uD68D-\uD6A7\uD6A9-\uD6C3\uD6C5-\uD6DF\uD6E1-\uD6FB\uD6FD-\uD717\uD719-\uD733\uD735-\uD74F\uD751-\uD76B\uD76D-\uD787\uD789-\uD7A3][\u11A8-\u11FF\uD7CB-\uD7FB]*|[\u1100-\u115F\uA960-\uA97C]+|[\u11A8-\u11FF\uD7CB-\uD7FB]+|[\u00A9\u00AE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9-\u21AA\u231A-\u231B\u2328\u23CF\u23E9-\u23EC\u23ED-\u23EE\u23EF\u23F0\u23F1-\u23F2\u23F3\u23F8-\u23FA\u24C2\u25AA-\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2601\u2602-\u2603\u2604\u260E\u2611\u2614-\u2615\u2618\u261D\u2620\u2622-\u2623\u2626\u262A\u262E\u262F\u2638-\u2639\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665-\u2666\u2668\u267B\u267E\u267F\u2692\u2693\u2694\u2695\u2696-\u2697\u2699\u269B-\u269C\u26A0-\u26A1\u26A7\u26AA-\u26AB\u26B0-\u26B1\u26BD-\u26BE\u26C4-\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F1\u26F2-\u26F3\u26F4\u26F5\u26F7-\u26F9\u26FA\u26FD\u2702\u2705\u2708-\u270C\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733-\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934-\u2935\u2B05-\u2B07\u2B1B-\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299\u{1F004}\u{1F02C}-\u{1F02F}\u{1F094}-\u{1F09F}\u{1F0AF}-\u{1F0B0}\u{1F0C0}\u{1F0CF}\u{1F0D0}\u{1F0F6}-\u{1F0FF}\u{1F170}-\u{1F171}\u{1F17E}-\u{1F17F}\u{1F18E}\u{1F191}-\u{1F19A}\u{1F1AE}-\u{1F1E5}\u{1F201}-\u{1F202}\u{1F203}-\u{1F20F}\u{1F21A}\u{1F22F}\u{1F232}-\u{1F23A}\u{1F23C}-\u{1F23F}\u{1F249}-\u{1F24F}\u{1F250}-\u{1F251}\u{1F252}-\u{1F25F}\u{1F266}-\u{1F2FF}\u{1F300}-\u{1F30C}\u{1F30D}-\u{1F30E}\u{1F30F}\u{1F310}\u{1F311}\u{1F312}\u{1F313}-\u{1F315}\u{1F316}-\u{1F318}\u{1F319}\u{1F31A}\u{1F31B}\u{1F31C}\u{1F31D}-\u{1F31E}\u{1F31F}-\u{1F320}\u{1F321}\u{1F324}-\u{1F32C}\u{1F32D}-\u{1F32F}\u{1F330}-\u{1F331}\u{1F332}-\u{1F333}\u{1F334}-\u{1F335}\u{1F336}\u{1F337}-\u{1F34A}\u{1F34B}\u{1F34C}-\u{1F34F}\u{1F350}\u{1F351}-\u{1F37B}\u{1F37C}\u{1F37D}\u{1F37E}-\u{1F37F}\u{1F380}-\u{1F393}\u{1F396}-\u{1F397}\u{1F399}-\u{1F39B}\u{1F39E}-\u{1F39F}\u{1F3A0}-\u{1F3C4}\u{1F3C5}\u{1F3C6}\u{1F3C7}\u{1F3C8}\u{1F3C9}\u{1F3CA}\u{1F3CB}-\u{1F3CE}\u{1F3CF}-\u{1F3D3}\u{1F3D4}-\u{1F3DF}\u{1F3E0}-\u{1F3E3}\u{1F3E4}\u{1F3E5}-\u{1F3F0}\u{1F3F3}\u{1F3F4}\u{1F3F5}\u{1F3F7}\u{1F3F8}-\u{1F3FA}\u{1F400}-\u{1F407}\u{1F408}\u{1F409}-\u{1F40B}\u{1F40C}-\u{1F40E}\u{1F40F}-\u{1F410}\u{1F411}-\u{1F412}\u{1F413}\u{1F414}\u{1F415}\u{1F416}\u{1F417}-\u{1F429}\u{1F42A}\u{1F42B}-\u{1F43E}\u{1F43F}\u{1F440}\u{1F441}\u{1F442}-\u{1F464}\u{1F465}\u{1F466}-\u{1F46B}\u{1F46C}-\u{1F46D}\u{1F46E}-\u{1F4AC}\u{1F4AD}\u{1F4AE}-\u{1F4B5}\u{1F4B6}-\u{1F4B7}\u{1F4B8}-\u{1F4EB}\u{1F4EC}-\u{1F4ED}\u{1F4EE}\u{1F4EF}\u{1F4F0}-\u{1F4F4}\u{1F4F5}\u{1F4F6}-\u{1F4F7}\u{1F4F8}\u{1F4F9}-\u{1F4FC}\u{1F4FD}\u{1F4FF}-\u{1F502}\u{1F503}\u{1F504}-\u{1F507}\u{1F508}\u{1F509}\u{1F50A}-\u{1F514}\u{1F515}\u{1F516}-\u{1F52B}\u{1F52C}-\u{1F52D}\u{1F52E}-\u{1F53D}\u{1F549}-\u{1F54A}\u{1F54B}-\u{1F54E}\u{1F550}-\u{1F55B}\u{1F55C}-\u{1F567}\u{1F56F}-\u{1F570}\u{1F573}-\u{1F579}\u{1F57A}\u{1F587}\u{1F58A}-\u{1F58D}\u{1F590}\u{1F595}-\u{1F596}\u{1F5A4}\u{1F5A5}\u{1F5A8}\u{1F5B1}-\u{1F5B2}\u{1F5BC}\u{1F5C2}-\u{1F5C4}\u{1F5D1}-\u{1F5D3}\u{1F5DC}-\u{1F5DE}\u{1F5E1}\u{1F5E3}\u{1F5E8}\u{1F5EF}\u{1F5F3}\u{1F5FA}\u{1F5FB}-\u{1F5FF}\u{1F600}\u{1F601}-\u{1F606}\u{1F607}-\u{1F608}\u{1F609}-\u{1F60D}\u{1F60E}\u{1F60F}\u{1F610}\u{1F611}\u{1F612}-\u{1F614}\u{1F615}\u{1F616}\u{1F617}\u{1F618}\u{1F619}\u{1F61A}\u{1F61B}\u{1F61C}-\u{1F61E}\u{1F61F}\u{1F620}-\u{1F625}\u{1F626}-\u{1F627}\u{1F628}-\u{1F62B}\u{1F62C}\u{1F62D}\u{1F62E}-\u{1F62F}\u{1F630}-\u{1F633}\u{1F634}\u{1F635}\u{1F636}\u{1F637}-\u{1F640}\u{1F641}-\u{1F644}\u{1F645}-\u{1F64F}\u{1F680}\u{1F681}-\u{1F682}\u{1F683}-\u{1F685}\u{1F686}\u{1F687}\u{1F688}\u{1F689}\u{1F68A}-\u{1F68B}\u{1F68C}\u{1F68D}\u{1F68E}\u{1F68F}\u{1F690}\u{1F691}-\u{1F693}\u{1F694}\u{1F695}\u{1F696}\u{1F697}\u{1F698}\u{1F699}-\u{1F69A}\u{1F69B}-\u{1F6A1}\u{1F6A2}\u{1F6A3}\u{1F6A4}-\u{1F6A5}\u{1F6A6}\u{1F6A7}-\u{1F6AD}\u{1F6AE}-\u{1F6B1}\u{1F6B2}\u{1F6B3}-\u{1F6B5}\u{1F6B6}\u{1F6B7}-\u{1F6B8}\u{1F6B9}-\u{1F6BE}\u{1F6BF}\u{1F6C0}\u{1F6C1}-\u{1F6C5}\u{1F6CB}\u{1F6CC}\u{1F6CD}-\u{1F6CF}\u{1F6D0}\u{1F6D1}-\u{1F6D2}\u{1F6D5}\u{1F6D6}-\u{1F6D7}\u{1F6D8}\u{1F6D9}-\u{1F6DB}\u{1F6DC}\u{1F6DD}-\u{1F6DF}\u{1F6E0}-\u{1F6E5}\u{1F6E9}\u{1F6EB}-\u{1F6EC}\u{1F6ED}-\u{1F6EF}\u{1F6F0}\u{1F6F3}\u{1F6F4}-\u{1F6F6}\u{1F6F7}-\u{1F6F8}\u{1F6F9}\u{1F6FA}\u{1F6FB}-\u{1F6FC}\u{1F6FD}-\u{1F6FF}\u{1F7DA}-\u{1F7DF}\u{1F7E0}-\u{1F7EB}\u{1F7EC}-\u{1F7EF}\u{1F7F0}\u{1F7F1}-\u{1F7FF}\u{1F80C}-\u{1F80F}\u{1F848}-\u{1F84F}\u{1F85A}-\u{1F85F}\u{1F888}-\u{1F88F}\u{1F8AE}-\u{1F8AF}\u{1F8BC}-\u{1F8BF}\u{1F8C2}-\u{1F8CF}\u{1F8D9}-\u{1F8FF}\u{1F90C}\u{1F90D}-\u{1F90F}\u{1F910}-\u{1F918}\u{1F919}-\u{1F91E}\u{1F91F}\u{1F920}-\u{1F927}\u{1F928}-\u{1F92F}\u{1F930}\u{1F931}-\u{1F932}\u{1F933}-\u{1F93A}\u{1F93C}-\u{1F93E}\u{1F93F}\u{1F940}-\u{1F945}\u{1F947}-\u{1F94B}\u{1F94C}\u{1F94D}-\u{1F94F}\u{1F950}-\u{1F95E}\u{1F95F}-\u{1F96B}\u{1F96C}-\u{1F970}\u{1F971}\u{1F972}\u{1F973}-\u{1F976}\u{1F977}-\u{1F978}\u{1F979}\u{1F97A}\u{1F97B}\u{1F97C}-\u{1F97F}\u{1F980}-\u{1F984}\u{1F985}-\u{1F991}\u{1F992}-\u{1F997}\u{1F998}-\u{1F9A2}\u{1F9A3}-\u{1F9A4}\u{1F9A5}-\u{1F9AA}\u{1F9AB}-\u{1F9AD}\u{1F9AE}-\u{1F9AF}\u{1F9B0}-\u{1F9B9}\u{1F9BA}-\u{1F9BF}\u{1F9C0}\u{1F9C1}-\u{1F9C2}\u{1F9C3}-\u{1F9CA}\u{1F9CB}\u{1F9CC}\u{1F9CD}-\u{1F9CF}\u{1F9D0}-\u{1F9E6}\u{1F9E7}-\u{1F9FF}\u{1FA58}-\u{1FA5F}\u{1FA6E}-\u{1FA6F}\u{1FA70}-\u{1FA73}\u{1FA74}\u{1FA75}-\u{1FA77}\u{1FA78}-\u{1FA7A}\u{1FA7B}-\u{1FA7C}\u{1FA7D}-\u{1FA7F}\u{1FA80}-\u{1FA82}\u{1FA83}-\u{1FA86}\u{1FA87}-\u{1FA88}\u{1FA89}\u{1FA8A}\u{1FA8B}-\u{1FA8D}\u{1FA8E}\u{1FA8F}\u{1FA90}-\u{1FA95}\u{1FA96}-\u{1FAA8}\u{1FAA9}-\u{1FAAC}\u{1FAAD}-\u{1FAAF}\u{1FAB0}-\u{1FAB6}\u{1FAB7}-\u{1FABA}\u{1FABB}-\u{1FABD}\u{1FABE}\u{1FABF}\u{1FAC0}-\u{1FAC2}\u{1FAC3}-\u{1FAC5}\u{1FAC6}\u{1FAC7}\u{1FAC8}\u{1FAC9}-\u{1FACC}\u{1FACD}\u{1FACE}-\u{1FACF}\u{1FAD0}-\u{1FAD6}\u{1FAD7}-\u{1FAD9}\u{1FADA}-\u{1FADB}\u{1FADC}\u{1FADD}-\u{1FADE}\u{1FADF}\u{1FAE0}-\u{1FAE7}\u{1FAE8}\u{1FAE9}\u{1FAEA}\u{1FAEB}-\u{1FAEE}\u{1FAEF}\u{1FAF0}-\u{1FAF6}\u{1FAF7}-\u{1FAF8}\u{1FAF9}-\u{1FAFF}\u{1FC00}-\u{1FFFD}](?:[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u0897-\u089F\u08CA-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962-\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2-\u09E3\u09FE\u0A01-\u0A02\u0A3C\u0A41-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A70-\u0A71\u0A75\u0A81-\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7-\u0AC8\u0ACD\u0AE2-\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B55-\u0B56\u0B57\u0B62-\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C04\u0C3C\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56\u0C62-\u0C63\u0C81\u0CBC\u0CBF\u0CC0\u0CC2\u0CC6\u0CC7-\u0CC8\u0CCA-\u0CCB\u0CCC-\u0CCD\u0CD5-\u0CD6\u0CE2-\u0CE3\u0D00-\u0D01\u0D3B-\u0D3C\u0D3E\u0D41-\u0D44\u0D4D\u0D57\u0D62-\u0D63\u0D81\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECE\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86-\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039-\u103A\u103D-\u103E\u1058-\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085-\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1715\u1732-\u1733\u1734\u1752-\u1753\u1772-\u1773\u17B4-\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u180F\u1885-\u1886\u18A9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193B\u1A17-\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1ABE\u1ABF-\u1ADD\u1AE0-\u1AEB\u1B00-\u1B03\u1B34\u1B35\u1B36-\u1B3A\u1B3B\u1B3C\u1B3D\u1B42\u1B43-\u1B44\u1B6B-\u1B73\u1B80-\u1B81\u1BA2-\u1BA5\u1BA8-\u1BA9\u1BAA\u1BAB-\u1BAD\u1BE6\u1BE8-\u1BE9\u1BED\u1BEF-\u1BF1\u1BF2-\u1BF3\u1C2C-\u1C33\u1C36-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8-\u1CF9\u1DC0-\u1DFF\u200C\u20D0-\u20DC\u20DD-\u20E0\u20E1\u20E2-\u20E4\u20E5-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u302E-\u302F\u3099-\u309A\uA66F\uA670-\uA672\uA674-\uA67D\uA69E-\uA69F\uA6F0-\uA6F1\uA802\uA806\uA80B\uA825-\uA826\uA82C\uA8C4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA953\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC-\uA9BD\uA9C0\uA9E5\uAA29-\uAA2E\uAA31-\uAA32\uAA35-\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7-\uAAB8\uAABE-\uAABF\uAAC1\uAAEC-\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E-\uFF9F\u{101FD}\u{102E0}\u{10376}-\u{1037A}\u{10A01}-\u{10A03}\u{10A05}-\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10A3F}\u{10AE5}-\u{10AE6}\u{10D24}-\u{10D27}\u{10D69}-\u{10D6D}\u{10EAB}-\u{10EAC}\u{10EFA}-\u{10EFF}\u{10F46}-\u{10F50}\u{10F82}-\u{10F85}\u{11001}\u{11038}-\u{11046}\u{11070}\u{11073}-\u{11074}\u{1107F}-\u{11081}\u{110B3}-\u{110B6}\u{110B9}-\u{110BA}\u{110C2}\u{11100}-\u{11102}\u{11127}-\u{1112B}\u{1112D}-\u{11134}\u{11173}\u{11180}-\u{11181}\u{111B6}-\u{111BE}\u{111C0}\u{111C9}-\u{111CC}\u{111CF}\u{1122F}-\u{11231}\u{11234}\u{11235}\u{11236}-\u{11237}\u{1123E}\u{11241}\u{112DF}\u{112E3}-\u{112EA}\u{11300}-\u{11301}\u{1133B}-\u{1133C}\u{1133E}\u{11340}\u{1134D}\u{11357}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{113B8}\u{113BB}-\u{113C0}\u{113C2}\u{113C5}\u{113C7}-\u{113C9}\u{113CE}\u{113CF}\u{113D0}\u{113D2}\u{113E1}-\u{113E2}\u{11438}-\u{1143F}\u{11442}-\u{11444}\u{11446}\u{1145E}\u{114B0}\u{114B3}-\u{114B8}\u{114BA}\u{114BD}\u{114BF}-\u{114C0}\u{114C2}-\u{114C3}\u{115AF}\u{115B2}-\u{115B5}\u{115BC}-\u{115BD}\u{115BF}-\u{115C0}\u{115DC}-\u{115DD}\u{11633}-\u{1163A}\u{1163D}\u{1163F}-\u{11640}\u{116AB}\u{116AD}\u{116B0}-\u{116B5}\u{116B6}\u{116B7}\u{1171D}\u{1171F}\u{11722}-\u{11725}\u{11727}-\u{1172B}\u{1182F}-\u{11837}\u{11839}-\u{1183A}\u{11930}\u{1193B}-\u{1193C}\u{1193D}\u{1193E}\u{11943}\u{119D4}-\u{119D7}\u{119DA}-\u{119DB}\u{119E0}\u{11A01}-\u{11A0A}\u{11A33}-\u{11A38}\u{11A3B}-\u{11A3E}\u{11A47}\u{11A51}-\u{11A56}\u{11A59}-\u{11A5B}\u{11A8A}-\u{11A96}\u{11A98}-\u{11A99}\u{11B60}\u{11B62}-\u{11B64}\u{11B66}\u{11C30}-\u{11C36}\u{11C38}-\u{11C3D}\u{11C3F}\u{11C92}-\u{11CA7}\u{11CAA}-\u{11CB0}\u{11CB2}-\u{11CB3}\u{11CB5}-\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}-\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D90}-\u{11D91}\u{11D95}\u{11D97}\u{11EF3}-\u{11EF4}\u{11F00}-\u{11F01}\u{11F36}-\u{11F3A}\u{11F40}\u{11F41}\u{11F42}\u{11F5A}\u{13440}\u{13447}-\u{13455}\u{1611E}-\u{16129}\u{1612D}-\u{1612F}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F8F}-\u{16F92}\u{16FE4}\u{16FF0}-\u{16FF1}\u{1BC9D}-\u{1BC9E}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1D165}-\u{1D166}\u{1D167}-\u{1D169}\u{1D16D}-\u{1D172}\u{1D17B}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D242}-\u{1D244}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}-\u{1E024}\u{1E026}-\u{1E02A}\u{1E08F}\u{1E130}-\u{1E136}\u{1E2AE}\u{1E2EC}-\u{1E2EF}\u{1E4EC}-\u{1E4EF}\u{1E5EE}-\u{1E5EF}\u{1E6E3}\u{1E6E6}\u{1E6EE}-\u{1E6EF}\u{1E6F5}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94A}\u{1F3FB}-\u{1F3FF}\u{E0020}-\u{E007F}\u{E0100}-\u{E01EF}]*\u200D[\u00A9\u00AE\u203C\u2049\u2122\u2139\u2194-\u2199\u21A9-\u21AA\u231A-\u231B\u2328\u23CF\u23E9-\u23EC\u23ED-\u23EE\u23EF\u23F0\u23F1-\u23F2\u23F3\u23F8-\u23FA\u24C2\u25AA-\u25AB\u25B6\u25C0\u25FB-\u25FE\u2600-\u2601\u2602-\u2603\u2604\u260E\u2611\u2614-\u2615\u2618\u261D\u2620\u2622-\u2623\u2626\u262A\u262E\u262F\u2638-\u2639\u263A\u2640\u2642\u2648-\u2653\u265F\u2660\u2663\u2665-\u2666\u2668\u267B\u267E\u267F\u2692\u2693\u2694\u2695\u2696-\u2697\u2699\u269B-\u269C\u26A0-\u26A1\u26A7\u26AA-\u26AB\u26B0-\u26B1\u26BD-\u26BE\u26C4-\u26C5\u26C8\u26CE\u26CF\u26D1\u26D3\u26D4\u26E9\u26EA\u26F0-\u26F1\u26F2-\u26F3\u26F4\u26F5\u26F7-\u26F9\u26FA\u26FD\u2702\u2705\u2708-\u270C\u270D\u270F\u2712\u2714\u2716\u271D\u2721\u2728\u2733-\u2734\u2744\u2747\u274C\u274E\u2753-\u2755\u2757\u2763\u2764\u2795-\u2797\u27A1\u27B0\u27BF\u2934-\u2935\u2B05-\u2B07\u2B1B-\u2B1C\u2B50\u2B55\u3030\u303D\u3297\u3299\u{1F004}\u{1F02C}-\u{1F02F}\u{1F094}-\u{1F09F}\u{1F0AF}-\u{1F0B0}\u{1F0C0}\u{1F0CF}\u{1F0D0}\u{1F0F6}-\u{1F0FF}\u{1F170}-\u{1F171}\u{1F17E}-\u{1F17F}\u{1F18E}\u{1F191}-\u{1F19A}\u{1F1AE}-\u{1F1E5}\u{1F201}-\u{1F202}\u{1F203}-\u{1F20F}\u{1F21A}\u{1F22F}\u{1F232}-\u{1F23A}\u{1F23C}-\u{1F23F}\u{1F249}-\u{1F24F}\u{1F250}-\u{1F251}\u{1F252}-\u{1F25F}\u{1F266}-\u{1F2FF}\u{1F300}-\u{1F30C}\u{1F30D}-\u{1F30E}\u{1F30F}\u{1F310}\u{1F311}\u{1F312}\u{1F313}-\u{1F315}\u{1F316}-\u{1F318}\u{1F319}\u{1F31A}\u{1F31B}\u{1F31C}\u{1F31D}-\u{1F31E}\u{1F31F}-\u{1F320}\u{1F321}\u{1F324}-\u{1F32C}\u{1F32D}-\u{1F32F}\u{1F330}-\u{1F331}\u{1F332}-\u{1F333}\u{1F334}-\u{1F335}\u{1F336}\u{1F337}-\u{1F34A}\u{1F34B}\u{1F34C}-\u{1F34F}\u{1F350}\u{1F351}-\u{1F37B}\u{1F37C}\u{1F37D}\u{1F37E}-\u{1F37F}\u{1F380}-\u{1F393}\u{1F396}-\u{1F397}\u{1F399}-\u{1F39B}\u{1F39E}-\u{1F39F}\u{1F3A0}-\u{1F3C4}\u{1F3C5}\u{1F3C6}\u{1F3C7}\u{1F3C8}\u{1F3C9}\u{1F3CA}\u{1F3CB}-\u{1F3CE}\u{1F3CF}-\u{1F3D3}\u{1F3D4}-\u{1F3DF}\u{1F3E0}-\u{1F3E3}\u{1F3E4}\u{1F3E5}-\u{1F3F0}\u{1F3F3}\u{1F3F4}\u{1F3F5}\u{1F3F7}\u{1F3F8}-\u{1F3FA}\u{1F400}-\u{1F407}\u{1F408}\u{1F409}-\u{1F40B}\u{1F40C}-\u{1F40E}\u{1F40F}-\u{1F410}\u{1F411}-\u{1F412}\u{1F413}\u{1F414}\u{1F415}\u{1F416}\u{1F417}-\u{1F429}\u{1F42A}\u{1F42B}-\u{1F43E}\u{1F43F}\u{1F440}\u{1F441}\u{1F442}-\u{1F464}\u{1F465}\u{1F466}-\u{1F46B}\u{1F46C}-\u{1F46D}\u{1F46E}-\u{1F4AC}\u{1F4AD}\u{1F4AE}-\u{1F4B5}\u{1F4B6}-\u{1F4B7}\u{1F4B8}-\u{1F4EB}\u{1F4EC}-\u{1F4ED}\u{1F4EE}\u{1F4EF}\u{1F4F0}-\u{1F4F4}\u{1F4F5}\u{1F4F6}-\u{1F4F7}\u{1F4F8}\u{1F4F9}-\u{1F4FC}\u{1F4FD}\u{1F4FF}-\u{1F502}\u{1F503}\u{1F504}-\u{1F507}\u{1F508}\u{1F509}\u{1F50A}-\u{1F514}\u{1F515}\u{1F516}-\u{1F52B}\u{1F52C}-\u{1F52D}\u{1F52E}-\u{1F53D}\u{1F549}-\u{1F54A}\u{1F54B}-\u{1F54E}\u{1F550}-\u{1F55B}\u{1F55C}-\u{1F567}\u{1F56F}-\u{1F570}\u{1F573}-\u{1F579}\u{1F57A}\u{1F587}\u{1F58A}-\u{1F58D}\u{1F590}\u{1F595}-\u{1F596}\u{1F5A4}\u{1F5A5}\u{1F5A8}\u{1F5B1}-\u{1F5B2}\u{1F5BC}\u{1F5C2}-\u{1F5C4}\u{1F5D1}-\u{1F5D3}\u{1F5DC}-\u{1F5DE}\u{1F5E1}\u{1F5E3}\u{1F5E8}\u{1F5EF}\u{1F5F3}\u{1F5FA}\u{1F5FB}-\u{1F5FF}\u{1F600}\u{1F601}-\u{1F606}\u{1F607}-\u{1F608}\u{1F609}-\u{1F60D}\u{1F60E}\u{1F60F}\u{1F610}\u{1F611}\u{1F612}-\u{1F614}\u{1F615}\u{1F616}\u{1F617}\u{1F618}\u{1F619}\u{1F61A}\u{1F61B}\u{1F61C}-\u{1F61E}\u{1F61F}\u{1F620}-\u{1F625}\u{1F626}-\u{1F627}\u{1F628}-\u{1F62B}\u{1F62C}\u{1F62D}\u{1F62E}-\u{1F62F}\u{1F630}-\u{1F633}\u{1F634}\u{1F635}\u{1F636}\u{1F637}-\u{1F640}\u{1F641}-\u{1F644}\u{1F645}-\u{1F64F}\u{1F680}\u{1F681}-\u{1F682}\u{1F683}-\u{1F685}\u{1F686}\u{1F687}\u{1F688}\u{1F689}\u{1F68A}-\u{1F68B}\u{1F68C}\u{1F68D}\u{1F68E}\u{1F68F}\u{1F690}\u{1F691}-\u{1F693}\u{1F694}\u{1F695}\u{1F696}\u{1F697}\u{1F698}\u{1F699}-\u{1F69A}\u{1F69B}-\u{1F6A1}\u{1F6A2}\u{1F6A3}\u{1F6A4}-\u{1F6A5}\u{1F6A6}\u{1F6A7}-\u{1F6AD}\u{1F6AE}-\u{1F6B1}\u{1F6B2}\u{1F6B3}-\u{1F6B5}\u{1F6B6}\u{1F6B7}-\u{1F6B8}\u{1F6B9}-\u{1F6BE}\u{1F6BF}\u{1F6C0}\u{1F6C1}-\u{1F6C5}\u{1F6CB}\u{1F6CC}\u{1F6CD}-\u{1F6CF}\u{1F6D0}\u{1F6D1}-\u{1F6D2}\u{1F6D5}\u{1F6D6}-\u{1F6D7}\u{1F6D8}\u{1F6D9}-\u{1F6DB}\u{1F6DC}\u{1F6DD}-\u{1F6DF}\u{1F6E0}-\u{1F6E5}\u{1F6E9}\u{1F6EB}-\u{1F6EC}\u{1F6ED}-\u{1F6EF}\u{1F6F0}\u{1F6F3}\u{1F6F4}-\u{1F6F6}\u{1F6F7}-\u{1F6F8}\u{1F6F9}\u{1F6FA}\u{1F6FB}-\u{1F6FC}\u{1F6FD}-\u{1F6FF}\u{1F7DA}-\u{1F7DF}\u{1F7E0}-\u{1F7EB}\u{1F7EC}-\u{1F7EF}\u{1F7F0}\u{1F7F1}-\u{1F7FF}\u{1F80C}-\u{1F80F}\u{1F848}-\u{1F84F}\u{1F85A}-\u{1F85F}\u{1F888}-\u{1F88F}\u{1F8AE}-\u{1F8AF}\u{1F8BC}-\u{1F8BF}\u{1F8C2}-\u{1F8CF}\u{1F8D9}-\u{1F8FF}\u{1F90C}\u{1F90D}-\u{1F90F}\u{1F910}-\u{1F918}\u{1F919}-\u{1F91E}\u{1F91F}\u{1F920}-\u{1F927}\u{1F928}-\u{1F92F}\u{1F930}\u{1F931}-\u{1F932}\u{1F933}-\u{1F93A}\u{1F93C}-\u{1F93E}\u{1F93F}\u{1F940}-\u{1F945}\u{1F947}-\u{1F94B}\u{1F94C}\u{1F94D}-\u{1F94F}\u{1F950}-\u{1F95E}\u{1F95F}-\u{1F96B}\u{1F96C}-\u{1F970}\u{1F971}\u{1F972}\u{1F973}-\u{1F976}\u{1F977}-\u{1F978}\u{1F979}\u{1F97A}\u{1F97B}\u{1F97C}-\u{1F97F}\u{1F980}-\u{1F984}\u{1F985}-\u{1F991}\u{1F992}-\u{1F997}\u{1F998}-\u{1F9A2}\u{1F9A3}-\u{1F9A4}\u{1F9A5}-\u{1F9AA}\u{1F9AB}-\u{1F9AD}\u{1F9AE}-\u{1F9AF}\u{1F9B0}-\u{1F9B9}\u{1F9BA}-\u{1F9BF}\u{1F9C0}\u{1F9C1}-\u{1F9C2}\u{1F9C3}-\u{1F9CA}\u{1F9CB}\u{1F9CC}\u{1F9CD}-\u{1F9CF}\u{1F9D0}-\u{1F9E6}\u{1F9E7}-\u{1F9FF}\u{1FA58}-\u{1FA5F}\u{1FA6E}-\u{1FA6F}\u{1FA70}-\u{1FA73}\u{1FA74}\u{1FA75}-\u{1FA77}\u{1FA78}-\u{1FA7A}\u{1FA7B}-\u{1FA7C}\u{1FA7D}-\u{1FA7F}\u{1FA80}-\u{1FA82}\u{1FA83}-\u{1FA86}\u{1FA87}-\u{1FA88}\u{1FA89}\u{1FA8A}\u{1FA8B}-\u{1FA8D}\u{1FA8E}\u{1FA8F}\u{1FA90}-\u{1FA95}\u{1FA96}-\u{1FAA8}\u{1FAA9}-\u{1FAAC}\u{1FAAD}-\u{1FAAF}\u{1FAB0}-\u{1FAB6}\u{1FAB7}-\u{1FABA}\u{1FABB}-\u{1FABD}\u{1FABE}\u{1FABF}\u{1FAC0}-\u{1FAC2}\u{1FAC3}-\u{1FAC5}\u{1FAC6}\u{1FAC7}\u{1FAC8}\u{1FAC9}-\u{1FACC}\u{1FACD}\u{1FACE}-\u{1FACF}\u{1FAD0}-\u{1FAD6}\u{1FAD7}-\u{1FAD9}\u{1FADA}-\u{1FADB}\u{1FADC}\u{1FADD}-\u{1FADE}\u{1FADF}\u{1FAE0}-\u{1FAE7}\u{1FAE8}\u{1FAE9}\u{1FAEA}\u{1FAEB}-\u{1FAEE}\u{1FAEF}\u{1FAF0}-\u{1FAF6}\u{1FAF7}-\u{1FAF8}\u{1FAF9}-\u{1FAFF}\u{1FC00}-\u{1FFFD}])*|[\u{1F1E6}-\u{1F1FF}]{2}|[\u0915-\u0939\u0958-\u095F\u0978-\u097F\u0995-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09DC-\u09DD\u09DF\u09F0-\u09F1\u0A95-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0AF9\u0B15-\u0B28\u0B2A-\u0B30\u0B32-\u0B33\u0B35-\u0B39\u0B5C-\u0B5D\u0B5F\u0B71\u0C15-\u0C28\u0C2A-\u0C39\u0C58-\u0C5A\u0D15-\u0D3A\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065-\u1066\u106E-\u1070\u1075-\u1081\u108E\u1780-\u17B3\u1A20-\u1A54\u1B0B-\u1B0C\u1B13-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE-\u1BAF\u1BBB-\u1BBD\uA989-\uA98B\uA98F-\uA9B2\uA9E0-\uA9E4\uA9E7-\uA9EF\uA9FA-\uA9FE\uAA60-\uAA6F\uAA71-\uAA73\uAA7A\uAA7E-\uAA7F\uAAE0-\uAAEA\uABC0-\uABDA\u{10A00}\u{10A10}-\u{10A13}\u{10A15}-\u{10A17}\u{10A19}-\u{10A35}\u{11103}-\u{11126}\u{11144}\u{11147}\u{11380}-\u{11389}\u{1138B}\u{1138E}\u{11390}-\u{113B5}\u{11900}-\u{11906}\u{11909}\u{1190C}-\u{11913}\u{11915}-\u{11916}\u{11918}-\u{1192F}\u{11A00}\u{11A0B}-\u{11A32}\u{11A50}\u{11A5C}-\u{11A83}\u{11F04}-\u{11F10}\u{11F12}-\u{11F33}](?:[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u0897-\u089F\u08CA-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u0951-\u0957\u0962-\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09D7\u09E2-\u09E3\u09FE\u0A01-\u0A02\u0A3C\u0A41-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A70-\u0A71\u0A75\u0A81-\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7-\u0AC8\u0AE2-\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B55-\u0B56\u0B57\u0B62-\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C04\u0C3C\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4C\u0C55-\u0C56\u0C62-\u0C63\u0C81\u0CBC\u0CBF\u0CC0\u0CC2\u0CC6\u0CC7-\u0CC8\u0CCA-\u0CCB\u0CCC-\u0CCD\u0CD5-\u0CD6\u0CE2-\u0CE3\u0D00-\u0D01\u0D3B-\u0D3C\u0D3E\u0D41-\u0D44\u0D57\u0D62-\u0D63\u0D81\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECE\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86-\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u103A\u103D-\u103E\u1058-\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085-\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1715\u1732-\u1733\u1734\u1752-\u1753\u1772-\u1773\u17B4-\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D1\u17D3\u17DD\u180B-\u180D\u180F\u1885-\u1886\u18A9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193B\u1A17-\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1ABE\u1ABF-\u1ADD\u1AE0-\u1AEB\u1B00-\u1B03\u1B34\u1B35\u1B36-\u1B3A\u1B3B\u1B3C\u1B3D\u1B42\u1B43\u1B6B-\u1B73\u1B80-\u1B81\u1BA2-\u1BA5\u1BA8-\u1BA9\u1BAA\u1BAC-\u1BAD\u1BE6\u1BE8-\u1BE9\u1BED\u1BEF-\u1BF1\u1BF2-\u1BF3\u1C2C-\u1C33\u1C36-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8-\u1CF9\u1DC0-\u1DFF\u200D\u20D0-\u20DC\u20DD-\u20E0\u20E1\u20E2-\u20E4\u20E5-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u302E-\u302F\u3099-\u309A\uA66F\uA670-\uA672\uA674-\uA67D\uA69E-\uA69F\uA6F0-\uA6F1\uA802\uA806\uA80B\uA825-\uA826\uA82C\uA8C4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA953\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC-\uA9BD\uA9E5\uAA29-\uAA2E\uAA31-\uAA32\uAA35-\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7-\uAAB8\uAABE-\uAABF\uAAC1\uAAEC-\uAAED\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E-\uFF9F\u{101FD}\u{102E0}\u{10376}-\u{1037A}\u{10A01}-\u{10A03}\u{10A05}-\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10AE5}-\u{10AE6}\u{10D24}-\u{10D27}\u{10D69}-\u{10D6D}\u{10EAB}-\u{10EAC}\u{10EFA}-\u{10EFF}\u{10F46}-\u{10F50}\u{10F82}-\u{10F85}\u{11001}\u{11038}-\u{11046}\u{11070}\u{11073}-\u{11074}\u{1107F}-\u{11081}\u{110B3}-\u{110B6}\u{110B9}-\u{110BA}\u{110C2}\u{11100}-\u{11102}\u{11127}-\u{1112B}\u{1112D}-\u{11132}\u{11134}\u{11173}\u{11180}-\u{11181}\u{111B6}-\u{111BE}\u{111C0}\u{111C9}-\u{111CC}\u{111CF}\u{1122F}-\u{11231}\u{11234}\u{11235}\u{11236}-\u{11237}\u{1123E}\u{11241}\u{112DF}\u{112E3}-\u{112EA}\u{11300}-\u{11301}\u{1133B}-\u{1133C}\u{1133E}\u{11340}\u{1134D}\u{11357}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{113B8}\u{113BB}-\u{113C0}\u{113C2}\u{113C5}\u{113C7}-\u{113C9}\u{113CE}\u{113CF}\u{113D2}\u{113E1}-\u{113E2}\u{11438}-\u{1143F}\u{11442}-\u{11444}\u{11446}\u{1145E}\u{114B0}\u{114B3}-\u{114B8}\u{114BA}\u{114BD}\u{114BF}-\u{114C0}\u{114C2}-\u{114C3}\u{115AF}\u{115B2}-\u{115B5}\u{115BC}-\u{115BD}\u{115BF}-\u{115C0}\u{115DC}-\u{115DD}\u{11633}-\u{1163A}\u{1163D}\u{1163F}-\u{11640}\u{116AB}\u{116AD}\u{116B0}-\u{116B5}\u{116B6}\u{116B7}\u{1171D}\u{1171F}\u{11722}-\u{11725}\u{11727}-\u{1172B}\u{1182F}-\u{11837}\u{11839}-\u{1183A}\u{11930}\u{1193B}-\u{1193C}\u{1193D}\u{11943}\u{119D4}-\u{119D7}\u{119DA}-\u{119DB}\u{119E0}\u{11A01}-\u{11A0A}\u{11A33}-\u{11A38}\u{11A3B}-\u{11A3E}\u{11A51}-\u{11A56}\u{11A59}-\u{11A5B}\u{11A8A}-\u{11A96}\u{11A98}\u{11B60}\u{11B62}-\u{11B64}\u{11B66}\u{11C30}-\u{11C36}\u{11C38}-\u{11C3D}\u{11C3F}\u{11C92}-\u{11CA7}\u{11CAA}-\u{11CB0}\u{11CB2}-\u{11CB3}\u{11CB5}-\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}-\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D90}-\u{11D91}\u{11D95}\u{11D97}\u{11EF3}-\u{11EF4}\u{11F00}-\u{11F01}\u{11F36}-\u{11F3A}\u{11F40}\u{11F41}\u{11F5A}\u{13440}\u{13447}-\u{13455}\u{1611E}-\u{16129}\u{1612D}-\u{1612F}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F8F}-\u{16F92}\u{16FE4}\u{16FF0}-\u{16FF1}\u{1BC9D}-\u{1BC9E}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1D165}-\u{1D166}\u{1D167}-\u{1D169}\u{1D16D}-\u{1D172}\u{1D17B}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D242}-\u{1D244}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}-\u{1E024}\u{1E026}-\u{1E02A}\u{1E08F}\u{1E130}-\u{1E136}\u{1E2AE}\u{1E2EC}-\u{1E2EF}\u{1E4EC}-\u{1E4EF}\u{1E5EE}-\u{1E5EF}\u{1E6E3}\u{1E6E6}\u{1E6EE}-\u{1E6EF}\u{1E6F5}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94A}\u{1F3FB}-\u{1F3FF}\u{E0020}-\u{E007F}\u{E0100}-\u{E01EF}\u094D\u09CD\u0ACD\u0B4D\u0C4D\u0D4D\u1039\u17D2\u1A60\u1B44\u1BAB\uA9C0\uAAF6\u{10A3F}\u{11133}\u{113D0}\u{1193E}\u{11A47}\u{11A99}\u{11F42}]*[\u094D\u09CD\u0ACD\u0B4D\u0C4D\u0D4D\u1039\u17D2\u1A60\u1B44\u1BAB\uA9C0\uAAF6\u{10A3F}\u{11133}\u{113D0}\u{1193E}\u{11A47}\u{11A99}\u{11F42}][\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u0897-\u089F\u08CA-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u0951-\u0957\u0962-\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09D7\u09E2-\u09E3\u09FE\u0A01-\u0A02\u0A3C\u0A41-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A70-\u0A71\u0A75\u0A81-\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7-\u0AC8\u0AE2-\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B55-\u0B56\u0B57\u0B62-\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C04\u0C3C\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4C\u0C55-\u0C56\u0C62-\u0C63\u0C81\u0CBC\u0CBF\u0CC0\u0CC2\u0CC6\u0CC7-\u0CC8\u0CCA-\u0CCB\u0CCC-\u0CCD\u0CD5-\u0CD6\u0CE2-\u0CE3\u0D00-\u0D01\u0D3B-\u0D3C\u0D3E\u0D41-\u0D44\u0D57\u0D62-\u0D63\u0D81\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECE\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86-\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u103A\u103D-\u103E\u1058-\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085-\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1715\u1732-\u1733\u1734\u1752-\u1753\u1772-\u1773\u17B4-\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D1\u17D3\u17DD\u180B-\u180D\u180F\u1885-\u1886\u18A9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193B\u1A17-\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1ABE\u1ABF-\u1ADD\u1AE0-\u1AEB\u1B00-\u1B03\u1B34\u1B35\u1B36-\u1B3A\u1B3B\u1B3C\u1B3D\u1B42\u1B43\u1B6B-\u1B73\u1B80-\u1B81\u1BA2-\u1BA5\u1BA8-\u1BA9\u1BAA\u1BAC-\u1BAD\u1BE6\u1BE8-\u1BE9\u1BED\u1BEF-\u1BF1\u1BF2-\u1BF3\u1C2C-\u1C33\u1C36-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8-\u1CF9\u1DC0-\u1DFF\u200D\u20D0-\u20DC\u20DD-\u20E0\u20E1\u20E2-\u20E4\u20E5-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u302E-\u302F\u3099-\u309A\uA66F\uA670-\uA672\uA674-\uA67D\uA69E-\uA69F\uA6F0-\uA6F1\uA802\uA806\uA80B\uA825-\uA826\uA82C\uA8C4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA953\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC-\uA9BD\uA9E5\uAA29-\uAA2E\uAA31-\uAA32\uAA35-\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7-\uAAB8\uAABE-\uAABF\uAAC1\uAAEC-\uAAED\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E-\uFF9F\u{101FD}\u{102E0}\u{10376}-\u{1037A}\u{10A01}-\u{10A03}\u{10A05}-\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10AE5}-\u{10AE6}\u{10D24}-\u{10D27}\u{10D69}-\u{10D6D}\u{10EAB}-\u{10EAC}\u{10EFA}-\u{10EFF}\u{10F46}-\u{10F50}\u{10F82}-\u{10F85}\u{11001}\u{11038}-\u{11046}\u{11070}\u{11073}-\u{11074}\u{1107F}-\u{11081}\u{110B3}-\u{110B6}\u{110B9}-\u{110BA}\u{110C2}\u{11100}-\u{11102}\u{11127}-\u{1112B}\u{1112D}-\u{11132}\u{11134}\u{11173}\u{11180}-\u{11181}\u{111B6}-\u{111BE}\u{111C0}\u{111C9}-\u{111CC}\u{111CF}\u{1122F}-\u{11231}\u{11234}\u{11235}\u{11236}-\u{11237}\u{1123E}\u{11241}\u{112DF}\u{112E3}-\u{112EA}\u{11300}-\u{11301}\u{1133B}-\u{1133C}\u{1133E}\u{11340}\u{1134D}\u{11357}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{113B8}\u{113BB}-\u{113C0}\u{113C2}\u{113C5}\u{113C7}-\u{113C9}\u{113CE}\u{113CF}\u{113D2}\u{113E1}-\u{113E2}\u{11438}-\u{1143F}\u{11442}-\u{11444}\u{11446}\u{1145E}\u{114B0}\u{114B3}-\u{114B8}\u{114BA}\u{114BD}\u{114BF}-\u{114C0}\u{114C2}-\u{114C3}\u{115AF}\u{115B2}-\u{115B5}\u{115BC}-\u{115BD}\u{115BF}-\u{115C0}\u{115DC}-\u{115DD}\u{11633}-\u{1163A}\u{1163D}\u{1163F}-\u{11640}\u{116AB}\u{116AD}\u{116B0}-\u{116B5}\u{116B6}\u{116B7}\u{1171D}\u{1171F}\u{11722}-\u{11725}\u{11727}-\u{1172B}\u{1182F}-\u{11837}\u{11839}-\u{1183A}\u{11930}\u{1193B}-\u{1193C}\u{1193D}\u{11943}\u{119D4}-\u{119D7}\u{119DA}-\u{119DB}\u{119E0}\u{11A01}-\u{11A0A}\u{11A33}-\u{11A38}\u{11A3B}-\u{11A3E}\u{11A51}-\u{11A56}\u{11A59}-\u{11A5B}\u{11A8A}-\u{11A96}\u{11A98}\u{11B60}\u{11B62}-\u{11B64}\u{11B66}\u{11C30}-\u{11C36}\u{11C38}-\u{11C3D}\u{11C3F}\u{11C92}-\u{11CA7}\u{11CAA}-\u{11CB0}\u{11CB2}-\u{11CB3}\u{11CB5}-\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}-\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D90}-\u{11D91}\u{11D95}\u{11D97}\u{11EF3}-\u{11EF4}\u{11F00}-\u{11F01}\u{11F36}-\u{11F3A}\u{11F40}\u{11F41}\u{11F5A}\u{13440}\u{13447}-\u{13455}\u{1611E}-\u{16129}\u{1612D}-\u{1612F}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F8F}-\u{16F92}\u{16FE4}\u{16FF0}-\u{16FF1}\u{1BC9D}-\u{1BC9E}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1D165}-\u{1D166}\u{1D167}-\u{1D169}\u{1D16D}-\u{1D172}\u{1D17B}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D242}-\u{1D244}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}-\u{1E024}\u{1E026}-\u{1E02A}\u{1E08F}\u{1E130}-\u{1E136}\u{1E2AE}\u{1E2EC}-\u{1E2EF}\u{1E4EC}-\u{1E4EF}\u{1E5EE}-\u{1E5EF}\u{1E6E3}\u{1E6E6}\u{1E6EE}-\u{1E6EF}\u{1E6F5}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94A}\u{1F3FB}-\u{1F3FF}\u{E0020}-\u{E007F}\u{E0100}-\u{E01EF}\u094D\u09CD\u0ACD\u0B4D\u0C4D\u0D4D\u1039\u17D2\u1A60\u1B44\u1BAB\uA9C0\uAAF6\u{10A3F}\u{11133}\u{113D0}\u{1193E}\u{11A47}\u{11A99}\u{11F42}]*[\u0915-\u0939\u0958-\u095F\u0978-\u097F\u0995-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09DC-\u09DD\u09DF\u09F0-\u09F1\u0A95-\u0AA8\u0AAA-\u0AB0\u0AB2-\u0AB3\u0AB5-\u0AB9\u0AF9\u0B15-\u0B28\u0B2A-\u0B30\u0B32-\u0B33\u0B35-\u0B39\u0B5C-\u0B5D\u0B5F\u0B71\u0C15-\u0C28\u0C2A-\u0C39\u0C58-\u0C5A\u0D15-\u0D3A\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065-\u1066\u106E-\u1070\u1075-\u1081\u108E\u1780-\u17B3\u1A20-\u1A54\u1B0B-\u1B0C\u1B13-\u1B33\u1B45-\u1B4C\u1B83-\u1BA0\u1BAE-\u1BAF\u1BBB-\u1BBD\uA989-\uA98B\uA98F-\uA9B2\uA9E0-\uA9E4\uA9E7-\uA9EF\uA9FA-\uA9FE\uAA60-\uAA6F\uAA71-\uAA73\uAA7A\uAA7E-\uAA7F\uAAE0-\uAAEA\uABC0-\uABDA\u{10A00}\u{10A10}-\u{10A13}\u{10A15}-\u{10A17}\u{10A19}-\u{10A35}\u{11103}-\u{11126}\u{11144}\u{11147}\u{11380}-\u{11389}\u{1138B}\u{1138E}\u{11390}-\u{113B5}\u{11900}-\u{11906}\u{11909}\u{1190C}-\u{11913}\u{11915}-\u{11916}\u{11918}-\u{1192F}\u{11A00}\u{11A0B}-\u{11A32}\u{11A50}\u{11A5C}-\u{11A83}\u{11F04}-\u{11F10}\u{11F12}-\u{11F33}])*|[^\u0000-\u0009\u000B-\u000C\u000E-\u001F\u007F-\u009F\u00AD\u061C\u180E\u200B\u200E-\u200F\u2028\u2029\u202A-\u202E\u2060-\u2064\u2065\u2066-\u206F\uFEFF\uFFF0-\uFFF8\uFFF9-\uFFFB\u{13430}-\u{1343F}\u{1BCA0}-\u{1BCA3}\u{1D173}-\u{1D17A}\u{E0000}\u{E0001}\u{E0002}-\u{E001F}\u{E0080}-\u{E00FF}\u{E01F0}-\u{E0FFF}\r\n])(?:[\u0300-\u036F\u0483-\u0487\u0488-\u0489\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u07FD\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u0897-\u089F\u08CA-\u08E1\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962-\u0963\u0981\u09BC\u09BE\u09C1-\u09C4\u09CD\u09D7\u09E2-\u09E3\u09FE\u0A01-\u0A02\u0A3C\u0A41-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A70-\u0A71\u0A75\u0A81-\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7-\u0AC8\u0ACD\u0AE2-\u0AE3\u0AFA-\u0AFF\u0B01\u0B3C\u0B3E\u0B3F\u0B41-\u0B44\u0B4D\u0B55-\u0B56\u0B57\u0B62-\u0B63\u0B82\u0BBE\u0BC0\u0BCD\u0BD7\u0C00\u0C04\u0C3C\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56\u0C62-\u0C63\u0C81\u0CBC\u0CBF\u0CC0\u0CC2\u0CC6\u0CC7-\u0CC8\u0CCA-\u0CCB\u0CCC-\u0CCD\u0CD5-\u0CD6\u0CE2-\u0CE3\u0D00-\u0D01\u0D3B-\u0D3C\u0D3E\u0D41-\u0D44\u0D4D\u0D57\u0D62-\u0D63\u0D81\u0DCA\u0DCF\u0DD2-\u0DD4\u0DD6\u0DDF\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EBC\u0EC8-\u0ECE\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86-\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039-\u103A\u103D-\u103E\u1058-\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085-\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1715\u1732-\u1733\u1734\u1752-\u1753\u1772-\u1773\u17B4-\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u180F\u1885-\u1886\u18A9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193B\u1A17-\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1ABE\u1ABF-\u1ADD\u1AE0-\u1AEB\u1B00-\u1B03\u1B34\u1B35\u1B36-\u1B3A\u1B3B\u1B3C\u1B3D\u1B42\u1B43-\u1B44\u1B6B-\u1B73\u1B80-\u1B81\u1BA2-\u1BA5\u1BA8-\u1BA9\u1BAA\u1BAB-\u1BAD\u1BE6\u1BE8-\u1BE9\u1BED\u1BEF-\u1BF1\u1BF2-\u1BF3\u1C2C-\u1C33\u1C36-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8-\u1CF9\u1DC0-\u1DFF\u200C\u20D0-\u20DC\u20DD-\u20E0\u20E1\u20E2-\u20E4\u20E5-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u302E-\u302F\u3099-\u309A\uA66F\uA670-\uA672\uA674-\uA67D\uA69E-\uA69F\uA6F0-\uA6F1\uA802\uA806\uA80B\uA825-\uA826\uA82C\uA8C4-\uA8C5\uA8E0-\uA8F1\uA8FF\uA926-\uA92D\uA947-\uA951\uA953\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC-\uA9BD\uA9C0\uA9E5\uAA29-\uAA2E\uAA31-\uAA32\uAA35-\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7-\uAAB8\uAABE-\uAABF\uAAC1\uAAEC-\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F\uFF9E-\uFF9F\u{101FD}\u{102E0}\u{10376}-\u{1037A}\u{10A01}-\u{10A03}\u{10A05}-\u{10A06}\u{10A0C}-\u{10A0F}\u{10A38}-\u{10A3A}\u{10A3F}\u{10AE5}-\u{10AE6}\u{10D24}-\u{10D27}\u{10D69}-\u{10D6D}\u{10EAB}-\u{10EAC}\u{10EFA}-\u{10EFF}\u{10F46}-\u{10F50}\u{10F82}-\u{10F85}\u{11001}\u{11038}-\u{11046}\u{11070}\u{11073}-\u{11074}\u{1107F}-\u{11081}\u{110B3}-\u{110B6}\u{110B9}-\u{110BA}\u{110C2}\u{11100}-\u{11102}\u{11127}-\u{1112B}\u{1112D}-\u{11134}\u{11173}\u{11180}-\u{11181}\u{111B6}-\u{111BE}\u{111C0}\u{111C9}-\u{111CC}\u{111CF}\u{1122F}-\u{11231}\u{11234}\u{11235}\u{11236}-\u{11237}\u{1123E}\u{11241}\u{112DF}\u{112E3}-\u{112EA}\u{11300}-\u{11301}\u{1133B}-\u{1133C}\u{1133E}\u{11340}\u{1134D}\u{11357}\u{11366}-\u{1136C}\u{11370}-\u{11374}\u{113B8}\u{113BB}-\u{113C0}\u{113C2}\u{113C5}\u{113C7}-\u{113C9}\u{113CE}\u{113CF}\u{113D0}\u{113D2}\u{113E1}-\u{113E2}\u{11438}-\u{1143F}\u{11442}-\u{11444}\u{11446}\u{1145E}\u{114B0}\u{114B3}-\u{114B8}\u{114BA}\u{114BD}\u{114BF}-\u{114C0}\u{114C2}-\u{114C3}\u{115AF}\u{115B2}-\u{115B5}\u{115BC}-\u{115BD}\u{115BF}-\u{115C0}\u{115DC}-\u{115DD}\u{11633}-\u{1163A}\u{1163D}\u{1163F}-\u{11640}\u{116AB}\u{116AD}\u{116B0}-\u{116B5}\u{116B6}\u{116B7}\u{1171D}\u{1171F}\u{11722}-\u{11725}\u{11727}-\u{1172B}\u{1182F}-\u{11837}\u{11839}-\u{1183A}\u{11930}\u{1193B}-\u{1193C}\u{1193D}\u{1193E}\u{11943}\u{119D4}-\u{119D7}\u{119DA}-\u{119DB}\u{119E0}\u{11A01}-\u{11A0A}\u{11A33}-\u{11A38}\u{11A3B}-\u{11A3E}\u{11A47}\u{11A51}-\u{11A56}\u{11A59}-\u{11A5B}\u{11A8A}-\u{11A96}\u{11A98}-\u{11A99}\u{11B60}\u{11B62}-\u{11B64}\u{11B66}\u{11C30}-\u{11C36}\u{11C38}-\u{11C3D}\u{11C3F}\u{11C92}-\u{11CA7}\u{11CAA}-\u{11CB0}\u{11CB2}-\u{11CB3}\u{11CB5}-\u{11CB6}\u{11D31}-\u{11D36}\u{11D3A}\u{11D3C}-\u{11D3D}\u{11D3F}-\u{11D45}\u{11D47}\u{11D90}-\u{11D91}\u{11D95}\u{11D97}\u{11EF3}-\u{11EF4}\u{11F00}-\u{11F01}\u{11F36}-\u{11F3A}\u{11F40}\u{11F41}\u{11F42}\u{11F5A}\u{13440}\u{13447}-\u{13455}\u{1611E}-\u{16129}\u{1612D}-\u{1612F}\u{16AF0}-\u{16AF4}\u{16B30}-\u{16B36}\u{16F4F}\u{16F8F}-\u{16F92}\u{16FE4}\u{16FF0}-\u{16FF1}\u{1BC9D}-\u{1BC9E}\u{1CF00}-\u{1CF2D}\u{1CF30}-\u{1CF46}\u{1D165}-\u{1D166}\u{1D167}-\u{1D169}\u{1D16D}-\u{1D172}\u{1D17B}-\u{1D182}\u{1D185}-\u{1D18B}\u{1D1AA}-\u{1D1AD}\u{1D242}-\u{1D244}\u{1DA00}-\u{1DA36}\u{1DA3B}-\u{1DA6C}\u{1DA75}\u{1DA84}\u{1DA9B}-\u{1DA9F}\u{1DAA1}-\u{1DAAF}\u{1E000}-\u{1E006}\u{1E008}-\u{1E018}\u{1E01B}-\u{1E021}\u{1E023}-\u{1E024}\u{1E026}-\u{1E02A}\u{1E08F}\u{1E130}-\u{1E136}\u{1E2AE}\u{1E2EC}-\u{1E2EF}\u{1E4EC}-\u{1E4EF}\u{1E5EE}-\u{1E5EF}\u{1E6E3}\u{1E6E6}\u{1E6EE}-\u{1E6EF}\u{1E6F5}\u{1E8D0}-\u{1E8D6}\u{1E944}-\u{1E94A}\u{1F3FB}-\u{1F3FF}\u{E0020}-\u{E007F}\u{E0100}-\u{E01EF}]|\u200D|[\u0903\u093B\u093E-\u0940\u0949-\u094C\u094E-\u094F\u0982-\u0983\u09BF-\u09C0\u09C7-\u09C8\u09CB-\u09CC\u0A03\u0A3E-\u0A40\u0A83\u0ABE-\u0AC0\u0AC9\u0ACB-\u0ACC\u0B02-\u0B03\u0B40\u0B47-\u0B48\u0B4B-\u0B4C\u0BBF\u0BC1-\u0BC2\u0BC6-\u0BC8\u0BCA-\u0BCC\u0C01-\u0C03\u0C41-\u0C44\u0C82-\u0C83\u0CBE\u0CC1\u0CC3-\u0CC4\u0CF3\u0D02-\u0D03\u0D3F-\u0D40\u0D46-\u0D48\u0D4A-\u0D4C\u0D82-\u0D83\u0DD0-\u0DD1\u0DD8-\u0DDE\u0DF2-\u0DF3\u0E33\u0EB3\u0F3E-\u0F3F\u0F7F\u1031\u103B-\u103C\u1056-\u1057\u1084\u17B6\u17BE-\u17C5\u17C7-\u17C8\u1923-\u1926\u1929-\u192B\u1930-\u1931\u1933-\u1938\u1A19-\u1A1A\u1A55\u1A57\u1A6D-\u1A72\u1B04\u1B3E-\u1B41\u1B82\u1BA1\u1BA6-\u1BA7\u1BE7\u1BEA-\u1BEC\u1BEE\u1C24-\u1C2B\u1C34-\u1C35\u1CE1\u1CF7\uA823-\uA824\uA827\uA880-\uA881\uA8B4-\uA8C3\uA952\uA983\uA9B4-\uA9B5\uA9BA-\uA9BB\uA9BE-\uA9BF\uAA2F-\uAA30\uAA33-\uAA34\uAA4D\uAAEB\uAAEE-\uAAEF\uAAF5\uABE3-\uABE4\uABE6-\uABE7\uABE9-\uABEA\uABEC\u{11000}\u{11002}\u{11082}\u{110B0}-\u{110B2}\u{110B7}-\u{110B8}\u{1112C}\u{11145}-\u{11146}\u{11182}\u{111B3}-\u{111B5}\u{111BF}\u{111CE}\u{1122C}-\u{1122E}\u{11232}-\u{11233}\u{112E0}-\u{112E2}\u{11302}-\u{11303}\u{1133F}\u{11341}-\u{11344}\u{11347}-\u{11348}\u{1134B}-\u{1134C}\u{11362}-\u{11363}\u{113B9}-\u{113BA}\u{113CA}\u{113CC}-\u{113CD}\u{11435}-\u{11437}\u{11440}-\u{11441}\u{11445}\u{114B1}-\u{114B2}\u{114B9}\u{114BB}-\u{114BC}\u{114BE}\u{114C1}\u{115B0}-\u{115B1}\u{115B8}-\u{115BB}\u{115BE}\u{11630}-\u{11632}\u{1163B}-\u{1163C}\u{1163E}\u{116AC}\u{116AE}-\u{116AF}\u{1171E}\u{11726}\u{1182C}-\u{1182E}\u{11838}\u{11931}-\u{11935}\u{11937}-\u{11938}\u{11940}\u{11942}\u{119D1}-\u{119D3}\u{119DC}-\u{119DF}\u{119E4}\u{11A39}\u{11A57}-\u{11A58}\u{11A97}\u{11B61}\u{11B65}\u{11B67}\u{11C2F}\u{11C3E}\u{11CA9}\u{11CB1}\u{11CB4}\u{11D8A}-\u{11D8E}\u{11D93}-\u{11D94}\u{11D96}\u{11EF5}-\u{11EF6}\u{11F03}\u{11F34}-\u{11F35}\u{11F3E}-\u{11F3F}\u{1612A}-\u{1612C}\u{16F51}-\u{16F87}])*)/gu 2 +
+104
editor1/site/make-facets.js
··· 1 + import {grapheme_cost} from './lib/grapheme.js' 2 + import {getUtf8Length as utf8_length} from '@atcute/uint8array' 3 + 4 + export const MARKUP_TYPE = "com.example.richtext.facet#markup" 5 + 6 + export function has_strip(feature) { 7 + return MARKUP_TYPE===feature.$type 8 + } 9 + 10 + // this generates facets and also adds overlength property to the overlay spans 11 + export function finalize_facets(parserstate, mode, opts) { 12 + const events = parserstate.events 13 + let text = "" 14 + // first we need to build the entire string so we can calculate the length and see where it goes over the limit 15 + for (const event of events) { 16 + if ('text'==event.type) { 17 + text += event.text 18 + } else if ('open'==event.type) { 19 + if (has_strip(event.feature)) { 20 + text += event.feature.strip[0] 21 + } 22 + } else if ('close'==event.type) { 23 + if (has_strip(event.feature)) { 24 + text += event.feature.strip[1] 25 + } 26 + } 27 + } 28 + // cost: number 29 + // over_index: utf16 index 30 + const {cost, over_index} = grapheme_cost(text, opts.budget) 31 + 32 + // now do the actual stuff 33 + const open_features = new Set() 34 + const overlength_features = new Set() 35 + const facets = [] 36 + let total_length = 0 // utf16 index 37 + let bstart = 0, bcurr = 0 // utf8 indexes 38 + const flush_facet = ()=>{ 39 + if (bcurr <= bstart) 40 + return 41 + if (open_features.size) 42 + facets.push({ 43 + index: {byteStart:bstart, byteEnd:bcurr}, 44 + features: [...open_features].map(feature=>{ 45 + if (!has_strip(feature)) 46 + return feature 47 + 48 + // convert _surround into strip 49 + const feat = {...feature, strip:[0,0]} // copy 50 + if (bstart == feature.strip.bstart) // at start 51 + feat.strip[0] = feature.strip.surround[0].length 52 + if (bcurr == feature.strip.bend) { 53 + // at end 54 + feat.strip[1] = feature.strip.surround[1].length 55 + } else { 56 + // feat.cont = true (todo: i'd rather cont be the default, and we set a field if we don't want 2 adjacent features merged. or maybe we rely on the good old fashioned like if 2 features have all the same fields then they get merged. and we add a start/end field to each feature which will be the start/end of the original feature before splitting. which we can use to determine strip, so we can set the same strip on all of the created features and only apply it if the feature is at the start/end of the original) 57 + } 58 + return feat 59 + }), 60 + }) 61 + bstart = bcurr 62 + } 63 + for (const event of events) { 64 + if ('text'==event.type) { 65 + bcurr += utf8_length(event.text) 66 + total_length += event.text.length 67 + if (total_length > over_index) { 68 + const not_over = Math.max(0, event.text.length - (total_length - over_index)) 69 + event.overlength = not_over // note: here we assume that characters in text nodes have a 1:1 mapping to characters in their overlay node 70 + } 71 + } else if ('open'==event.type) { 72 + flush_facet() 73 + open_features.add(event.feature) 74 + if (has_strip(event.feature)) { 75 + const surround = event.feature.strip 76 + event.feature.strip = {surround, bstart: bcurr, bend: null} // hacky.. but this is a field we can safely use todo: clean this up. like store it on the event or something during the first pass idk. 77 + bcurr += utf8_length(surround[0]) 78 + // add both surrounds' lengths here, so we report length overruns earlier 79 + total_length += surround[0].length + surround[1].length 80 + } 81 + if (total_length > over_index) { 82 + event.overlength = 0 83 + overlength_features.add(event.feature) 84 + } 85 + } else if ('close'==event.type) { 86 + if (has_strip(event.feature)) { 87 + bcurr += event.feature.strip.surround[1].length 88 + // note: we added to `total_length` earlier 89 + event.feature.strip.bend = bcurr 90 + } 91 + flush_facet() 92 + open_features.delete(event.feature) 93 + if (overlength_features.has(event.feature)) { 94 + event.overlength = 0 95 + } 96 + } 97 + } 98 + 99 + return { 100 + highlight: parserstate.finalize_overlay_spans(), 101 + status: {cost, budget:opts.budget}, 102 + final: {text, facets}, 103 + } 104 + }
+213
editor1/site/parse-example.js
··· 1 + import ParserState from './parser-state.js' 2 + import {MARKUP_TYPE} from './make-facets.js' 3 + 4 + const r = String.raw 5 + const whitespace = r`\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2` 6 + const url = r`[-\w/%&=#+~@$*'!?,.;:]*` 7 + const url_final = r`[-\w/%&=#+~@$*']` 8 + const ipv6_address = r`(?:\[[0-9A-Fa-f:]+\])` 9 + const BIG_REGEX = RegExp([ 10 + r`\b(?<link>(?<link_begin>https?://)(?:${ipv6_address}?${url}${url_final}(?:[(]${url}[)](?:${url}${url_final})?)?|${ipv6_address}))(?<link_open>\[)?`, 11 + // alternative less strict link regex: 12 + //r`\b(?<link>(https?://|([a-zA-Z0-9-]+[.])+[a-zA-Z0-9-]{2,}(?=[/]))${url}${url_final}([(]${url}[)](${url}${url_final})?)?)(?<link_open>\[)?`, (todo: support ipv6 on this one) 13 + // urls must either have a scheme, or have a slash after the domain, to be considered. 14 + // ie we will never link common "word.word" mistakes. 15 + r`[##](?<hashtag>(?!\uFE0F)[^${whitespace}]*[^\p{P}${whitespace}])`, // note: must filter out #123 16 + r`@(?<mention>[-a-zA-Z0-9]+(?:[.][-a-zA-Z0-9]+)+)\b`, 17 + r`(?<style>[*][*]|[_][_]|[~][~]|[/])`, 18 + r`(?<close>\])`, 19 + r`(?<open>\[)`, 20 + r`\x60(?<code>.*?)\x60`, 21 + r`[\\]facet(?<facet>\[.*?\])\n\[`, 22 + r`[\\](?<tag_open>[a-z]+)\[`, 23 + r`[\\](?<escaped>.)`, 24 + ].join("|"), 'gu') 25 + 26 + const STYLE_SURROUND = { 27 + __proto__: null, 28 + italic: Object.freeze(["/","/"]), 29 + bold: Object.freeze(["**","**"]), 30 + underline: Object.freeze(["__","__"]), 31 + strikethrough: Object.freeze(["~~","~~"]), 32 + code: Object.freeze(["`","`"]), 33 + } 34 + const TAG_FEATURES = { 35 + __proto__: null, 36 + i: Object.freeze({$type:MARKUP_TYPE, style:'italic', strip:STYLE_SURROUND['italic']}), 37 + b: Object.freeze({$type:MARKUP_TYPE, style:'bold', strip:STYLE_SURROUND['bold']}), 38 + u: Object.freeze({$type:MARKUP_TYPE, style:'underline', strip:STYLE_SURROUND['underline']}), 39 + s: Object.freeze({$type:MARKUP_TYPE, style:'strikethrough', strip:STYLE_SURROUND['strikethrough']}), 40 + code: Object.freeze({$type:MARKUP_TYPE, style:'code', strip:STYLE_SURROUND['code']}), 41 + } 42 + 43 + // these regexes are used on a string containing the char before and the char after the style token. e.g. "a**b" we do STYLE_START.test("ab") 44 + const STYLE_START 45 + = /^[\s,][^\s,]|^['"}{(>|\[][^\s,'"]/ 46 + const STYLE_END 47 + = /^[^\s,][-\s.,:;!?'"}{)<\\|\]]/ 48 + const ITALIC_START 49 + = /^[\s,][^\s,/]|^['"}{(|\[][^\s,'"/<]/ 50 + const ITALIC_END 51 + = /^[^\s,/>][-\s.,:;!?'"}{)\\|\]]/ 52 + const STYLE_SYNTAX = { 53 + __proto__:null, 54 + '**': 'bold', 55 + '__': 'underline', 56 + '~~': 'strikethrough', 57 + '/': 'italic', 58 + } 59 + // todo: add a system to do this automatically (replacement for lookbehind which i am trying to avoid for now) 60 + const TAG_BEFORE 61 + = /^[\s(]/ 62 + const MENTION_BEFORE 63 + = /^[\s(]/ 64 + 65 + function parse_facet_json(text) { 66 + try { 67 + let json = JSON.parse(text) 68 + if (!(json instanceof Array)) 69 + return null 70 + for (let feature of json) { 71 + if ('string'!=typeof feature.$type) 72 + return null 73 + } 74 + return json 75 + } catch(e) { 76 + } 77 + return null 78 + } 79 + 80 + export default class EditorParser1 { 81 + options = {} 82 + constructor(opts) { 83 + this.options = opts 84 + } 85 + parse(text, mode='all') { 86 + const p = new ParserState(text, BIG_REGEX, this.options) 87 + const open_styles = new Map() // type → feature 88 + const open_brackets = [] // [feature] 89 + for (p.begin(); p.match; p.step()) { 90 + const match = p.match 91 + const start = match.index 92 + const end = start + match[0].length 93 + const g = match.groups 94 + 95 + decide: if (g.mention!=null) { 96 + if (!MENTION_BEFORE.test(p.charBefore(start)||"\n")) { 97 + p.reject() 98 + break decide 99 + } 100 + // @mention 101 + p.accept() 102 + let feature = {$type:"app.bsky.richtext.facet#mention", did:g.mention} 103 + p.element_direct(end, feature) 104 + } else if (g.link!=null) { 105 + let feature = {$type:"app.bsky.richtext.facet#link", uri:g.link} 106 + if (g.link_open!=null) { 107 + // link with text 108 + p.accept() 109 + p.event('open', end, feature) 110 + open_brackets.push(feature) 111 + } else { 112 + // standalone link 113 + p.accept() 114 + // ok so we have to pre-shorten link text, because like 115 + // if we dont remove the scheme part, bsky.app will truncate links a lot 116 + let sch = g.link_begin?.length ?? 0 117 + p.event('open', start+sch, feature) 118 + p.event('text', end) 119 + p.event('close', end, feature) 120 + } 121 + } else if (g.hashtag!=null) { 122 + if (/^\d+$/.test(g.hashtag) || !TAG_BEFORE.test(p.charBefore(start)||"\n")) { 123 + p.reject() 124 + break decide 125 + } 126 + // #hashtag 127 + p.accept() 128 + let feature = {$type:"app.bsky.richtext.facet#tag", tag:g.hashtag} 129 + p.element_direct(end, feature) 130 + } else if (g.style!=null) { 131 + let type = STYLE_SYNTAX[g.style] 132 + let before = p.charBefore(start)||"\n" 133 + let after = p.charAfter(end)||"\n" 134 + let feature = open_styles.get(type) 135 + if (!feature) { 136 + if (('italic'==type ? ITALIC_START : STYLE_START).test(before+after)) { 137 + // style open 138 + p.accept() 139 + feature = {$type:MARKUP_TYPE, style:type, strip:STYLE_SURROUND[type]} 140 + open_styles.set(type, feature) 141 + p.event('open', end, feature) 142 + break decide 143 + } 144 + } else { 145 + if (('italic'==type ? ITALIC_END : STYLE_END).test(before+after)) { 146 + // style close 147 + p.accept() 148 + p.event('close', end, feature) 149 + open_styles.delete(type) 150 + break decide 151 + } 152 + } 153 + p.reject() 154 + break decide 155 + } else if (g.open!=null) { 156 + // open bracket 157 + p.accept() 158 + p.event('text', end) 159 + open_brackets.push(null) 160 + } else if (g.close!=null) { 161 + if (!open_brackets.length) { 162 + p.reject() 163 + break decide 164 + } 165 + let feature = open_brackets.pop() 166 + if (feature) { 167 + // close bracket 168 + p.accept() 169 + p.event('close', end, feature) 170 + } else { 171 + // null close bracket 172 + p.accept() 173 + p.event('text', end) 174 + } 175 + } else if (g.code!=null) { 176 + // `code` (this is one match instead of separate open/close) 177 + p.accept() 178 + let feature = {$type:MARKUP_TYPE, style:'code', strip:STYLE_SURROUND['code']} 179 + p.event('open', start+1, feature) 180 + p.event('text', end-1) 181 + p.event('close', end, feature) 182 + } else if (g.tag_open!=null) { 183 + let feature = TAG_FEATURES[g.tag_open] 184 + if (feature) { 185 + // valid \tag[ 186 + p.accept() 187 + feature = {...feature} 188 + p.event('open', end, feature) 189 + open_brackets.push(feature) 190 + } else { 191 + // invalid \tag[ 192 + p.accept() 193 + p.event('text', end) 194 + } 195 + } else if (g.escaped!=null) { 196 + // escaped char 197 + p.accept() 198 + let feature = {$type:"#escape"} // only used for overlay 199 + p.event('text', end, feature, g.escaped) 200 + } else { 201 + // other (should never happen) 202 + p.reject() 203 + break decide 204 + } 205 + } 206 + p.finish() 207 + // cancel unclosed 208 + for (let feature of p.open_features.keys()) 209 + p.cancel_feature(feature) 210 + 211 + return this.options.finalize(p, mode, this.options) 212 + } 213 + }
+151
editor1/site/parser-state.js
··· 1 + // todo: parse modes, e.g.: 2 + // 'status' : overlay + status 3 + // 'preview' : overlay + status + preview 4 + // 'final' : final 5 + 6 + export default class ParserState { 7 + events = [] // [event] // note: each feature object should occur exactly 2 times in events 8 + open_features = new Map() // {feature → event} 9 + overlay_last = 0 // index into rawtext 10 + overlay_spans = [] // [temp_overlay] 11 + 12 + rawtext = "" 13 + regex = null 14 + 15 + options = {} 16 + 17 + match = null 18 + ok = false 19 + 20 + constructor(text, regex, opt) { 21 + this.rawtext = text 22 + this.regex = regex 23 + this.options = opt 24 + } 25 + // todo: make a single function that does overlay + event creation 26 + // also: what if we made overlay point to event rather than vice versa? 27 + // for most cases (when overlay and events are 1:1) 28 + // exceptions: 29 + // - event without an overlay (e.g. auto-closed tag) 30 + // - easy: no overlays point to the event 31 + // - overlay without an event (e.g. sets a flag, comment syntax) 32 + // - easy: overlay.event is null 33 + // - multiple overlays for a single event 34 + // - easy 35 + // - overlay that triggers multiple events 36 + // - uh oh! 37 + 38 + // hm i wonder if we can think about 39 + // events (particularly open events) as like 40 + // this is basically a text event, but with the promise that ok if this isnt cancelled, it will be a feature event instead. 41 + // so basically consider a bold open event. if that gets cancelled, what happens is it becomes a direct text event. i think let's make that a rule 42 + element_direct(end, feature) { 43 + this.event('open', undefined, feature) 44 + this.event('text', end) 45 + this.event('close', undefined, feature) 46 + } 47 + // what are the ways to eat a span of text: 48 + // - open_syntax (consumes text and creates an open event) 49 + // - close_syntax (consumes text and creates a close event) 50 + // - text_direct (consumes text and creates a text event with that text) 51 + // - text_indirect (consumes text and creates a text event with custom text) 52 + // actually let's just give each one a variant with custom text 53 + // so it'll be like 54 + // - type (open/close/text) 55 + // - end (how much text to consume) 56 + // - text - to set the text/oncancel field (optional, defaults to slice(last, end)) 57 + // - feature: for open/close: the feature (sets overlay owner field, type 1). for text: this is used for the overlay owner field corresp' type 2 58 + event(type, end=this.overlay_last, feature=null, text=this.slice(this.overlay_last, end)) { 59 + // add the event 60 + const event = {type, feature, text} 61 + this.events.push(event) 62 + // before 63 + if ('close'==type) 64 + this.open_features.delete(feature) 65 + // add the overlay span 66 + if (end > this.overlay_last) { 67 + const text = this.slice(this.overlay_last, end) 68 + const overlay = {text, features:new Set(this.open_features.keys()), owner:feature, event} 69 + this.overlay_spans.push(overlay) 70 + this.overlay_last = end 71 + } 72 + // after 73 + if ('open'==type) 74 + this.open_features.set(feature, event) 75 + } 76 + cancel_feature(feature) { 77 + const event = this.open_features.get(feature) 78 + this.open_features.delete(feature) 79 + event.type = 'text' 80 + event.feature = null 81 + feature._cancelled = true // eghh.. (this is ok because these are only used for overlay spans now) 82 + } 83 + begin() { 84 + this.regex.lastIndex = 0 // nnnn 85 + this.match = this.regex.exec(this.rawtext) 86 + } 87 + step(ok=true) { 88 + if (!ok) 89 + this.regex.lastIndex = this.match.index + (this.rawtext.codePointAt(this.match.index) < 0x10000 ? 1 : 2) 90 + this.match = this.regex.exec(this.rawtext) 91 + } 92 + finish() { 93 + const end = this.rawtext.length 94 + this.event('text', end) 95 + } 96 + accept() { 97 + this.ok = true 98 + const end = this.match.index 99 + this.event('text', end) 100 + } 101 + reject() { 102 + this.ok = false 103 + } 104 + // ** - .owner: the bold feature, .event: the bold start event 105 + // #test - .owner: the hashtag feature, .event: the text event 106 + // and in the first case, the event can be changed into a text event if the bold is cancelled 107 + // basically: 108 + // .owner is for spans which are markup syntax that caused a feature to be created, 109 + // .event is for overlength checking 110 + // .features are all the currently open features, mainly for text spans (todo: i dont like how we have to copy the features list for every span, and also filter out cancelled ones. let's find a nicer way someday) 111 + // also, why do we actually have to store the text strings; we could just store an end index until later. 112 + slice(start, end) { 113 + return this.rawtext.slice(start, end) 114 + } 115 + charAfter(start) { 116 + return this.rawtext.charAt(start) 117 + } 118 + charBefore(end) { 119 + return this.rawtext.charAt(end-1) 120 + } 121 + finalize_overlay_spans() { 122 + return this.overlay_spans.flatMap((span)=>{ 123 + let cn = "" 124 + for (const f of span.features) 125 + if (!f._cancelled) 126 + cn += " "+f.$type.split("#")[1] 127 + if (span.owner) { 128 + if (span.owner._cancelled) 129 + // if a syntax span had its owner feature cancelled, we still want to highlight that 130 + // so the user knows there /would/ be something here if they had closed the tag etc. 131 + cn += " cancelled" 132 + cn += " "+span.owner.$type.split("#")[1]+"-syntax" 133 + // todo: we have basically 2 kinds of owner highlighting: 134 + // 1: syntax which wont appear in the output. e.g. **test** highlighting on the **. this should use bg color 135 + // 2: "syntax" which will appear in the output. like, these chars Activated the parser but are also expected to be shown as text. e.g. #test highlighting on the entire thing. this should not use strong bg color but does need to be indicated somehow (underline, fg color, etc.). 136 + } 137 + if (span.event) { 138 + const over = span.event.overlength 139 + if (0===over) { 140 + cn += " overlength" 141 + } else if (over>0 && over<=span.text.length) { 142 + return [ 143 + [span.text.slice(0, over), cn], 144 + [span.text.slice(over), cn+" overlength"], 145 + ] 146 + } 147 + } 148 + return [[span.text, cn]] 149 + }) 150 + } 151 + }
+69
editor1/site/perf.js
··· 1 + import {Widget, HTML} from './template.js' 2 + export default class PerfGraph extends Widget { 3 + constructor(categories) { 4 + super() 5 + this.c2d = this.$canvas.getContext('2d') 6 + this.cx = null 7 + this.center = 100 8 + for (let c of categories) { 9 + let row = new.target.template_row() 10 + row.$th.textContent = c.color 11 + row.$th.style.backgroundColor = c.color 12 + row.$td.textContent = c.label 13 + this.$header.after(row.$root) 14 + } 15 + this.reset() 16 + } 17 + reset() { 18 + let c2d = this.c2d 19 + c2d.clearRect(10,0,500,200) 20 + c2d.fillStyle = 'black' 21 + c2d.fillRect(0,0,1,200) 22 + c2d.fillRect(0,100,9,1) 23 + for (let i=0; i<=50; i+=10) { 24 + c2d.fillRect(0,this.center - i*2 - 0.5,500,1) 25 + c2d.fillRect(0,this.center+1 + i*2 - 0.5,500,1) 26 + } 27 + this.cx=10 28 + } 29 + graph_time(t0, after, before) { 30 + let c2d = this.c2d 31 + this.cx++ 32 + if (this.cx>=500) 33 + this.reset() 34 + c2d.fillStyle = 'white' 35 + c2d.fillRect(this.cx,this.center,1,1) 36 + for (let i=after.length-1; i>=1; i-=2) { 37 + let t = after[i] 38 + c2d.fillStyle = after[i-1] 39 + c2d.fillRect(this.cx,this.center,1,-(t-t0)*2) 40 + } 41 + for (let i=before.length-1; i>=1; i-=2) { 42 + let t = before[i] 43 + if (t) { 44 + c2d.fillStyle = before[i-1] 45 + c2d.fillRect(this.cx,this.center+1,1,-(t-t0)*2) 46 + } 47 + } 48 + } 49 + set_status(text) { 50 + this.$status.textContent = text 51 + } 52 + } 53 + PerfGraph.template_row = HTML` 54 + <tr> 55 + <th $=th><td $=td> 56 + ` 57 + PerfGraph.template = HTML` 58 + <div class='Row'> 59 + <div class='Col'> 60 + <span $=status></span><br> 61 + <table> 62 + <tr $=header> 63 + <th> color <th> time 64 + </table> 65 + 10ms divisions 66 + </div> 67 + <canvas height=200 width=500 $=canvas style='width:max-content'></canvas> 68 + </div> 69 + `
+16
editor1/site/resource/fonts.css
··· 1 + @font-face { 2 + font-family: 'InterVariable'; 3 + src: url("https://web-cdn.bsky.app/static/media/InterVariable.c504db5c06caaf7cdfba.woff2") format('woff2'); 4 + font-feature-settings: 'ss04'; /* “Disambiguation (no slashed zero)” */ 5 + font-weight: 300 1000; 6 + font-style: normal; 7 + font-display: swap; 8 + } 9 + @font-face { 10 + font-family: 'InterVariable'; 11 + src: url("https://web-cdn.bsky.app/static/media/InterVariable-Italic.01dcbad1bac635f9c9cd.woff2") format('woff2'); 12 + font-feature-settings: 'ss04'; 13 + font-weight: 300 1000; 14 + font-style: italic; 15 + font-display: swap; 16 + }
+83
editor1/site/root.js
··· 1 + import PerfGraph from './perf.js' 2 + import Editor1 from './editor.js' 3 + import EditorParser1 from './parse-example.js' 4 + import {finalize_facets} from './make-facets.js' 5 + 6 + import {FacetDisplay, PostDisplay} from './facet-display.js' 7 + import LengthMeter from './length-meter.js' 8 + 9 + let fd = new FacetDisplay() 10 + let pd = new PostDisplay() 11 + let last_post = null 12 + function display() { 13 + if (last_post) { 14 + fd.show(last_post) 15 + pd.show(last_post, {render_markup: $styles.checked}) 16 + } 17 + } 18 + 19 + let LIMIT = 50 20 + let graph = new PerfGraph(Editor1.graph_categories) 21 + let meter = new LengthMeter() 22 + let parser = new EditorParser1({ 23 + finalize: finalize_facets, 24 + budget: LIMIT, 25 + }) 26 + let editor = new Editor1({ 27 + parser, 28 + graph, 29 + oninput(status, final) { 30 + meter.show(status.cost, status.budget) 31 + 32 + last_post = final 33 + window.setTimeout(display) 34 + }, 35 + preview: true, 36 + }) 37 + editor.set_styles(` 38 + textarea-overlay > [class*="-syntax"] { 39 + background: #afa; 40 + } 41 + textarea-overlay > .cancelled { 42 + background: yellow; 43 + } 44 + textarea-overlay > .link { 45 + color: blue; 46 + } 47 + textarea-overlay > .mention { 48 + color: blue; 49 + } 50 + textarea-overlay > .tag { 51 + color: blue; 52 + } 53 + textarea-overlay > .escape-syntax { 54 + background: #fda; 55 + } 56 + textarea-overlay > .overlength { 57 + background: #faa; 58 + } 59 + textarea-overlay > .error { 60 + color: #800; 61 + } 62 + textarea-overlay > .markup { 63 + text-decoration: underline; 64 + } 65 + `) 66 + 67 + window.go = function() { 68 + $graph.replaceWith(graph.$root) 69 + 70 + $meter.replaceWith(meter.$root) 71 + 72 + $editor.replaceWith(editor.$root) 73 + 74 + $facets.replaceWith(fd.$root) 75 + 76 + $post.replaceWith(pd.$root) 77 + 78 + $styles.onchange = ev=>{ 79 + display() 80 + } 81 + 82 + editor.run() 83 + }
+30
editor1/site/style.css
··· 1 + .name { color: cyan; } 2 + .comment { color: #0C0; } 3 + .commenttext { color: lime; } 4 + .tag { color: #0AF; } 5 + .doctype { color: silver; } 6 + .charref { color: magenta; } 7 + .key { color: orange; } 8 + .value { color: fuchsia; } 9 + .script { color: silver; } 10 + .rawtext { color: silver; } 11 + 12 + .prefix {color: lime;} 13 + .new {text-decoration: underline;} 14 + .suffix {color: blue;} 15 + 16 + span.word { 17 + color: #DEF; 18 + } 19 + span.comment { 20 + color: #a1f198; 21 + font-style: italic; 22 + } 23 + span.string { color: #9C9; } 24 + span.operator { color: #BB7; } 25 + span.keyword { color: #2494f0; } 26 + span.constant { color: #f359ab; } 27 + span.property { color: #D9B; } 28 + span.flow { color: #2494f0; } 29 + span.semicolon { color: #CF0; } 30 + span.assignment { color: goldenrod; }
+50
editor1/site/template.js
··· 1 + export let HTML = ([html])=>{ 2 + let temp = document.createElement('template') 3 + temp.innerHTML = html.replace(/\s*?\n\s*/g, "") 4 + let content = temp.content 5 + let root = content 6 + if (root.childNodes.length==1) 7 + root = root.firstChild 8 + 9 + let get_path = (root, node)=>{ 10 + let path = "" 11 + while (node!==root) { 12 + let parent = node.parentNode 13 + let pos = [].indexOf.call(parent.childNodes, node) 14 + path = ".firstChild"+".nextSibling".repeat(pos) + path 15 + node = parent 16 + } 17 + return path 18 + } 19 + 20 + let init = `const node=document.importNode(this, true) 21 + holder.$root=node` 22 + for (let node of content.querySelectorAll("[\\$]")) { 23 + let path = get_path(root, node) 24 + let id = node.getAttribute('$') 25 + node.removeAttribute('$') 26 + id = id.replace(/,/g, " = holder.$") 27 + init += ` 28 + holder.$${id} = node${path}` 29 + } 30 + init += ` 31 + return holder` 32 + let c = new Function("holder={}", init).bind(root) 33 + //c.prototype = {__proto__: null, template: root} 34 + return c 35 + } 36 + export class Widget { 37 + constructor() { 38 + new.target.template(this) 39 + } 40 + static set css(css) { 41 + if (this.$css) 42 + this.$css.textContent = css 43 + else { 44 + this.$css = document.createElement('style') 45 + this.$css.textContent = css 46 + document.head.insertBefore(this.$css, this.first_style) 47 + } 48 + } 49 + } 50 + Widget.first_style = document.head.querySelector('style') // can be null
+115
editor1/site/test.html
··· 1 + <!doctype html><meta charset=utf-8> 2 + 3 + <title>Editor with syntax highlighting</title> 4 + 5 + <script> 6 + let BASE_URL = new URL("./", window.location).href 7 + 8 + function alert_error(msg, message, source, line, col) { 9 + source = source.replace(BASE_URL, "📁 ") 10 + col = String(col).replace(/\d/g, c=>"₀₁₂₃₄₅₆₇₈₉"[c]) 11 + if (confirm(msg+"\n"+line+"."+col+" in "+source+"\n\n"+message+"\n\n[Cancel] = reload page, [Ok] = continue")) 12 + window.onerror = null 13 + else 14 + RELOAD() 15 + } 16 + 17 + /*window.onerror = (message, source, line, col, error)=>{ 18 + if (/^\w+-extension:/.test(source)) // browser extensions love to inject scripts that fail 19 + return 20 + (window.queueMicrotask || window.setTimeout)(()=>{ 21 + alert_error("🎆 ERROR DURING PAGE LOAD!", message, source, line, col) 22 + }) 23 + }*/ 24 + 25 + delete Object.prototype.toString 26 + delete Array.prototype.toString 27 + //x.y=z 28 + </script> 29 + <script src=bundle/root.js></script> 30 + 31 + <link rel=stylesheet href=./resource/fonts.css> 32 + <link rel=stylesheet href=./style.css> 33 + <link rel=stylesheet href=./common.css> 34 + <style> 35 + hl-textarea { 36 + display: block; 37 + max-width: -webkit-fill-available; 38 + max-width: -moz-available; 39 + max-width: stretch; 40 + width: 700px; 41 + --inner-padding: 4px; 42 + color: black; 43 + background: white; 44 + font-family: InterVariable, sans-serif; 45 + max-height: 100px; 46 + } 47 + bsky-post { 48 + font-family: InterVariable, sans-serif; 49 + text-underline-position: auto; 50 + text-underline-offset: 0.1em; 51 + } 52 + /* .mention { 53 + color: blue; 54 + } 55 + .link { 56 + color: blue; 57 + } 58 + .hashtag { 59 + color: blue; 60 + } 61 + .style { 62 + background: #aFa; 63 + box-shadow: 0.5px 1px #afa, -0.5px -1px #afa, -0.5px 1px #afa, 0.5px -1px #afa; 64 + } 65 + .open { 66 + border-top-left-radius: 4px; 67 + border-bottom-left-radius: 4px; 68 + } 69 + .close { 70 + border-top-right-radius: 4px; 71 + border-bottom-right-radius: 4px; 72 + } 73 + .open, .close { 74 + text-shadow: 1px 1px white; 75 + } 76 + .link { 77 + background: #aFa; 78 + box-shadow: 0.5px 1px #afa, -0.5px -1px #afa, -0.5px 1px #afa, 0.5px -1px #afa; 79 + } 80 + .tentative-style { 81 + background: yellow; 82 + } 83 + .tentative-link { 84 + background: yellow; 85 + } 86 + .inside { 87 + background: #dfd; 88 + text-shadow: 1px 1px white; 89 + }*/ 90 + length-meter { 91 + margin-top: 4px; 92 + margin-left: 10px; 93 + } 94 + </style> 95 + 96 + Input: 97 + <br id=$editor> 98 + <br id=$meter> 99 + <hr> 100 + Performance: 101 + <br id=$graph> 102 + <hr> 103 + <details open> 104 + <summary>Output</summary> 105 + <div>Output: (<label>Render Styles: <input type=checkbox checked id=$styles></label>)</div> 106 + <br id=$post> 107 + </details> 108 + <hr> 109 + <details> 110 + <summary>Facets</summary> 111 + <br id=$facets> 112 + </details> 113 + <hr> 114 + 115 + <script>go()</script>
+2 -1
package.json
··· 66 66 "@atcute/identity-resolver": "^1.2.2", 67 67 "@atcute/lexicons": "^1.2.9", 68 68 "@atcute/oauth-browser-client": "^3.0.0", 69 - "@atcute/tid": "^1.1.2" 69 + "@atcute/tid": "^1.1.2", 70 + "editor1": "file:./editor1" 70 71 } 71 72 }
+11
pnpm-lock.yaml
··· 35 35 '@atcute/tid': 36 36 specifier: ^1.1.2 37 37 version: 1.1.2 38 + editor1: 39 + specifier: file:./editor1 40 + version: file:editor1 38 41 devDependencies: 39 42 '@chromatic-com/storybook': 40 43 specifier: ^5.0.1 ··· 1326 1329 1327 1330 domutils@3.2.2: 1328 1331 resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} 1332 + 1333 + editor1@file:editor1: 1334 + resolution: {directory: editor1, type: directory} 1329 1335 1330 1336 enhanced-resolve@5.19.0: 1331 1337 resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} ··· 3470 3476 dom-serializer: 2.0.0 3471 3477 domelementtype: 2.3.0 3472 3478 domhandler: 5.0.3 3479 + 3480 + editor1@file:editor1: 3481 + dependencies: 3482 + '@atcute/bluesky-richtext-segmenter': 3.0.0 3483 + '@atcute/uint8array': 1.1.1 3473 3484 3474 3485 enhanced-resolve@5.19.0: 3475 3486 dependencies:
+67 -8
src/routes/+layout.svelte
··· 5 5 import { setUserContext, getUserContext } from '$lib/context'; 6 6 import * as TID from '@atcute/tid'; 7 7 import { getClient, login } from '$lib/atproto'; 8 + import { onMount } from 'svelte'; 8 9 9 10 let { children, data } = $props(); 10 11 ··· 17 18 let handle = $state(''); 18 19 let loggingIn = $state(false); 19 20 let composerDialog: HTMLDialogElement; 20 - let postContent = $state(''); 21 + let editorContainer: HTMLDivElement; 22 + let editor: any; 23 + 24 + onMount(async () => { 25 + const { HlTextarea, EditorParser1, finalize_facets } = await import('editor1'); 26 + 27 + const parser = new EditorParser1({ 28 + finalize: finalize_facets, 29 + budget: 300 30 + }); 31 + 32 + editor = new HlTextarea({ 33 + parser, 34 + oninput(status: any) {} 35 + }); 36 + 37 + editor.set_styles(String.raw` 38 + textarea-overlay > .link { 39 + color: rgb(32, 139, 254); 40 + } 41 + textarea-overlay > .mention { 42 + color: rgb(32, 139, 254); 43 + } 44 + textarea-overlay > .tag { 45 + color: rgb(32, 139, 254); 46 + } 47 + textarea-overlay > .escape-syntax { 48 + color: rgb(150, 150, 150); 49 + } 50 + textarea-overlay > .overlength { 51 + background: #faa; 52 + } 53 + textarea-overlay > .error { 54 + color: #800; 55 + } 56 + textarea-overlay > .markup { 57 + text-decoration: underline; 58 + } 59 + `); 60 + 61 + editorContainer.append(editor.$root); 62 + }); 21 63 22 64 async function handleLogin() { 23 65 if (!handle.trim()) return; ··· 35 77 throw new Error('need to be authenticated to make a post'); 36 78 } 37 79 80 + const { text, facets } = editor.get_data(); 81 + 38 82 const client = await getClient(); 39 83 const { ok } = await client.post('com.atproto.repo.createRecord', { 40 84 input: { 41 85 repo: user.profile.did, 42 86 collection: 'app.bsky.feed.post', 43 - rkey: TID.now(), // generates a sortable timestamp-based key 87 + rkey: TID.now(), 44 88 record: { 45 89 $type: 'app.bsky.feed.post', 46 - text: postContent, 90 + text, 91 + facets, 47 92 createdAt: new Date().toISOString(), 48 93 langs: ['en'] 49 94 } ··· 129 174 <Avatar user={user.profile} /> 130 175 </div> 131 176 {/if} 132 - <textarea 133 - class="m-[1px] mb-[11px] ml-[9px] min-h-[140px] grow resize-none p-[4px] text-[16.9px] leading-[24px]" 134 - placeholder="What's up?" 135 - bind:value={postContent} 136 - ></textarea> 177 + <div 178 + bind:this={editorContainer} 179 + class="composer-editor m-[1px] mb-[11px] ml-[9px] min-h-[140px] grow" 180 + ></div> 137 181 </main> 138 182 </form> 139 183 </dialog> ··· 158 202 } 159 203 dialog::backdrop { 160 204 background-color: rgba(0, 0, 0, 0.8); 205 + } 206 + .composer-editor :global(hl-textarea) { 207 + display: block; 208 + width: 100%; 209 + min-height: 140px; 210 + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 211 + font-size: 16.9px; 212 + line-height: 24px; 213 + border: none; 214 + margin: 0; 215 + padding: 4px; 216 + background: transparent; 217 + color: inherit; 218 + --inner-padding: 4px; 219 + outline: none; 161 220 } 162 221 </style>