Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

feat: compose bold/italic/underline markdown in post editor

Winter 229ed977 d5791fd9

+626 -16
+1
package.json
··· 189 189 "lodash.debounce": "^4.0.8", 190 190 "lodash.shuffle": "^4.2.0", 191 191 "lodash.throttle": "^4.1.1", 192 + "micromark": "^4.0.2", 192 193 "multiformats": "9.9.0", 193 194 "nanoid": "^5.0.5", 194 195 "normalize-url": "^8.0.0",
+15 -9
src/lib/api/index.ts
··· 23 23 import * as Hasher from 'multiformats/hashes/hasher' 24 24 25 25 import {isNetworkError} from '#/lib/strings/errors' 26 - import { 27 - parseMarkdownLinks, 28 - shortenLinks, 29 - stripInvalidMentions, 30 - } from '#/lib/strings/rich-text-manip' 26 + import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' 27 + import {parseMarkdownRichText} from '#/lib/strings/richtext-markdown' 31 28 import {logger} from '#/logger' 32 29 import {compressImage} from '#/state/gallery' 33 30 import { ··· 204 201 // Trim any trailing whitespace. 205 202 .trimEnd() 206 203 207 - const {text: parsedText, facets: markdownFacets} = 208 - parseMarkdownLinks(trimmedText) 204 + const { 205 + text: parsedText, 206 + facets: markdownFacets, 207 + escapedByteStarts, 208 + } = parseMarkdownRichText(trimmedText) 209 209 210 210 let rt = new RichText({text: parsedText}) 211 211 await rt.detectFacets(agent) 212 212 213 - if (markdownFacets.length > 0) { 213 + if (markdownFacets.length > 0 || escapedByteStarts.size > 0) { 214 + // Only filter auto-detected facets that conflict with explicit link facets; 215 + // bold/italic facets may coexist with mention/tag facets on the same range. 216 + const linkMarkdownFacets = markdownFacets.filter(mf => 217 + mf.features.some(f => f.$type === 'app.bsky.richtext.facet#link'), 218 + ) 214 219 const nonOverlapping = (rt.facets || []).filter(f => { 215 - return !markdownFacets.some(mf => { 220 + if (escapedByteStarts.has(f.index.byteStart)) return false 221 + return !linkMarkdownFacets.some(mf => { 216 222 return ( 217 223 (f.index.byteStart >= mf.index.byteStart && 218 224 f.index.byteStart < mf.index.byteEnd) ||
+270
src/lib/strings/richtext-markdown.ts
··· 1 + import {type AppBskyRichtextFacet} from '@atproto/api' 2 + import {parse, postprocess, preprocess} from 'micromark' 3 + 4 + type FormatKind = 'bold' | 'italic' | 'underline' | 'link' 5 + 6 + interface FormatRange { 7 + kind: FormatKind 8 + start: number 9 + end: number 10 + uri?: string 11 + } 12 + 13 + function mergeOverlapping( 14 + ranges: FormatRange[], 15 + kind: FormatKind, 16 + ): FormatRange[] { 17 + const filtered = ranges 18 + .filter(f => f.kind === kind) 19 + .sort((a, b) => a.start - b.start) 20 + const out: FormatRange[] = [] 21 + for (const r of filtered) { 22 + const prev = out[out.length - 1] 23 + if (prev && r.start <= prev.end) { 24 + prev.end = Math.max(prev.end, r.end) 25 + } else { 26 + out.push({...r}) 27 + } 28 + } 29 + return out 30 + } 31 + 32 + export function parseMarkdownEvents(text: string) { 33 + try { 34 + const chunks = preprocess()(text, undefined, true) 35 + return postprocess(parse().document().write(chunks)) 36 + } catch { 37 + return null 38 + } 39 + } 40 + 41 + export function parseMarkdownRichText(text: string): { 42 + text: string 43 + facets: AppBskyRichtextFacet.Main[] 44 + escapedByteStarts: Set<number> 45 + } { 46 + const events = parseMarkdownEvents(text) 47 + if (!events) return {text, facets: [], escapedByteStarts: new Set()} 48 + 49 + const stripRanges: {start: number; end: number}[] = [] 50 + const formatRanges: FormatRange[] = [] 51 + const escapedCharOrigPositions: number[] = [] 52 + 53 + // Links: labelText comes before resource in the event stream, so we 54 + // buffer link state per nesting level until we can emit the full range. 55 + const linkStack: { 56 + labelTextStart: number 57 + labelTextEnd: number 58 + uri: string 59 + }[] = [] 60 + // Track whether each strong span uses _ (underline) or * (bold). 61 + const strongKindStack: ('bold' | 'underline')[] = [] 62 + let awaitingStrongKind = false 63 + 64 + for (const [eventType, token] of events) { 65 + const {type} = token 66 + const start = token.start.offset 67 + const end = token.end.offset 68 + 69 + if (eventType === 'enter') { 70 + switch (type) { 71 + case 'emphasisSequence': 72 + case 'labelMarker': // [ and ] 73 + case 'resource': // (url) 74 + stripRanges.push({start, end}) 75 + break 76 + case 'strongSequence': 77 + if (awaitingStrongKind) { 78 + strongKindStack.push(text[start] === '_' ? 'underline' : 'bold') 79 + awaitingStrongKind = false 80 + } 81 + stripRanges.push({start, end}) 82 + break 83 + case 'strong': 84 + awaitingStrongKind = true 85 + break 86 + case 'characterEscape': 87 + stripRanges.push({start, end: start + 1}) // strip only the backslash 88 + escapedCharOrigPositions.push(start + 1) // position of the kept char 89 + break 90 + case 'link': 91 + linkStack.push({labelTextStart: -1, labelTextEnd: -1, uri: ''}) 92 + break 93 + case 'labelText': 94 + if (linkStack.length > 0) { 95 + const top = linkStack[linkStack.length - 1] 96 + top.labelTextStart = start 97 + top.labelTextEnd = end 98 + } 99 + break 100 + case 'resourceDestinationString': 101 + if (linkStack.length > 0) { 102 + let uri = text.slice(start, end) 103 + if ( 104 + !uri.startsWith('http://') && 105 + !uri.startsWith('https://') && 106 + !uri.startsWith('mailto:') 107 + ) { 108 + uri = `https://${uri}` 109 + } 110 + linkStack[linkStack.length - 1].uri = uri 111 + } 112 + break 113 + case 'emphasisText': 114 + formatRanges.push({kind: 'italic', start, end}) 115 + break 116 + case 'strongText': 117 + formatRanges.push({ 118 + kind: strongKindStack[strongKindStack.length - 1] ?? 'bold', 119 + start, 120 + end, 121 + }) 122 + break 123 + } 124 + } else { 125 + if (type === 'strong') { 126 + strongKindStack.pop() 127 + } else if (type === 'link') { 128 + const info = linkStack.pop() 129 + if (info && info.labelTextStart >= 0 && info.uri) { 130 + formatRanges.push({ 131 + kind: 'link', 132 + start: info.labelTextStart, 133 + end: info.labelTextEnd, 134 + uri: info.uri, 135 + }) 136 + } 137 + } 138 + } 139 + } 140 + 141 + if (stripRanges.length === 0) { 142 + return {text, facets: [], escapedByteStarts: new Set()} 143 + } 144 + 145 + stripRanges.sort((a, b) => a.start - b.start) 146 + 147 + // removedBefore[i] = total chars stripped from positions [0, i) 148 + // Computed by marking each delimiter's removal at its end position, 149 + // then prefix-summing. 150 + const removedBefore = new Int32Array(text.length + 2) 151 + for (const {start, end} of stripRanges) { 152 + if (end <= text.length) { 153 + removedBefore[end] += end - start 154 + } 155 + } 156 + for (let i = 1; i <= text.length; i++) { 157 + removedBefore[i] += removedBefore[i - 1] 158 + } 159 + 160 + const toStripped = (orig: number): number => orig - removedBefore[orig] 161 + 162 + // Build stripped text 163 + let stripped = '' 164 + let si = 0 165 + let i = 0 166 + while (i < text.length) { 167 + if (si < stripRanges.length && i === stripRanges[si].start) { 168 + i = stripRanges[si].end 169 + si++ 170 + } else { 171 + stripped += text[i] 172 + i++ 173 + } 174 + } 175 + 176 + // Build char-index → UTF-8-byte-offset map once so facet byte positions 177 + // don't require O(n) UnicodeString construction per boundary. 178 + const charToUtf8Byte = new Int32Array(stripped.length + 1) 179 + { 180 + let b = 0 181 + for (let c = 0; c < stripped.length; ) { 182 + charToUtf8Byte[c] = b 183 + const cp = stripped.codePointAt(c)! 184 + b += cp <= 0x7f ? 1 : cp <= 0x7ff ? 2 : cp <= 0xffff ? 3 : 4 185 + c += cp > 0xffff ? 2 : 1 186 + } 187 + charToUtf8Byte[stripped.length] = b 188 + } 189 + 190 + const escapedByteStarts = new Set( 191 + escapedCharOrigPositions.map(orig => charToUtf8Byte[toStripped(orig)]), 192 + ) 193 + 194 + if (formatRanges.length === 0) { 195 + return {text: stripped, facets: [], escapedByteStarts} 196 + } 197 + 198 + // Map format ranges to stripped char positions, discard empty ranges 199 + const mapped = formatRanges 200 + .map(f => ({ 201 + kind: f.kind, 202 + start: toStripped(f.start), 203 + end: toStripped(f.end), 204 + uri: f.uri, 205 + })) 206 + .filter(f => f.start < f.end) 207 + 208 + if (mapped.length === 0) { 209 + return {text: stripped, facets: [], escapedByteStarts} 210 + } 211 + 212 + // Merge overlapping ranges of the same kind (bold or italic) so that nested 213 + // same-kind emphasis (e.g. *emph *with emph* in it*) doesn't lose the outer 214 + // range when the inner one closes first in the sweep below. 215 + const mergedMapped = [ 216 + ...(['bold', 'italic', 'underline'] as const).flatMap(k => 217 + mergeOverlapping(mapped, k), 218 + ), 219 + ...mapped.filter(f => f.kind === 'link'), 220 + ] 221 + 222 + // Interval sweep to produce non-overlapping facets with merged features. 223 + // This correctly handles bold+italic on the same range (e.g. ***text***). 224 + type SweepEvent = { 225 + pos: number 226 + isOpen: boolean 227 + kind: FormatKind 228 + uri?: string 229 + } 230 + const sweep: SweepEvent[] = [] 231 + for (const f of mergedMapped) { 232 + sweep.push({pos: f.start, isOpen: true, kind: f.kind, uri: f.uri}) 233 + sweep.push({pos: f.end, isOpen: false, kind: f.kind, uri: f.uri}) 234 + } 235 + sweep.sort((a, b) => a.pos - b.pos || (a.isOpen ? -1 : 1)) 236 + 237 + const active = new Map<FormatKind, string | undefined>() 238 + let curPos = 0 239 + const facets: AppBskyRichtextFacet.Main[] = [] 240 + 241 + for (const ev of sweep) { 242 + if (ev.pos > curPos && active.size > 0) { 243 + const features: AppBskyRichtextFacet.Main['features'] = [] 244 + for (const kind of ['bold', 'italic', 'underline'] as const) { 245 + if (active.has(kind)) 246 + features.push({$type: `app.bsky.richtext.facet#${kind}`} as any) 247 + } 248 + if (active.has('link')) 249 + features.push({ 250 + $type: 'app.bsky.richtext.facet#link', 251 + uri: active.get('link')!, 252 + }) 253 + facets.push({ 254 + index: { 255 + byteStart: charToUtf8Byte[curPos], 256 + byteEnd: charToUtf8Byte[ev.pos], 257 + }, 258 + features, 259 + }) 260 + } 261 + curPos = ev.pos 262 + if (ev.isOpen) { 263 + active.set(ev.kind, ev.uri) 264 + } else { 265 + active.delete(ev.kind) 266 + } 267 + } 268 + 269 + return {text: stripped, facets, escapedByteStarts} 270 + }
+15
src/style.css
··· 114 114 .ProseMirror .autolink { 115 115 color: var(--mention-color, #ed5345); 116 116 } 117 + .ProseMirror .format-delimiter { 118 + opacity: 0.35; 119 + } 120 + .ProseMirror .format-bold { 121 + font-weight: 700; 122 + } 123 + .ProseMirror .format-italic { 124 + font-style: italic; 125 + } 126 + .ProseMirror .format-underline { 127 + text-decoration: underline; 128 + } 129 + .ProseMirror .format-italic + .format-delimiter { 130 + padding-left: 2px; 131 + } 117 132 /* OLLIE: TODO -- this is not accessible */ 118 133 /* Remove focus state on inputs */ 119 134 .ProseMirror-focused {
+2 -2
src/view/com/composer/Composer.tsx
··· 294 294 // Clear error when composer content changes, but only if all posts are 295 295 // back within the character limit. 296 296 const allPostsWithinLimit = thread.posts.every( 297 - post => post.richtext.graphemeLength <= MAX_DRAFT_GRAPHEME_LENGTH, 297 + post => post.shortenedGraphemeLength <= MAX_DRAFT_GRAPHEME_LENGTH, 298 298 ) 299 299 300 300 const activePost = thread.posts[composerState.activePostIndex] ··· 577 577 578 578 const validateDraftTextOrError = React.useCallback((): boolean => { 579 579 const tooLong = composerState.thread.posts.some( 580 - post => post.richtext.graphemeLength > MAX_DRAFT_GRAPHEME_LENGTH, 580 + post => post.shortenedGraphemeLength > MAX_DRAFT_GRAPHEME_LENGTH, 581 581 ) 582 582 if (tooLong) { 583 583 setError(
+3 -2
src/view/com/composer/state/composer.ts
··· 10 10 11 11 import {type SelfLabel} from '#/lib/moderation' 12 12 import {insertMentionAt} from '#/lib/strings/mention-manip' 13 - import {parseMarkdownLinks, shortenLinks} from '#/lib/strings/rich-text-manip' 13 + import {shortenLinks} from '#/lib/strings/rich-text-manip' 14 + import {parseMarkdownRichText} from '#/lib/strings/richtext-markdown' 14 15 import { 15 16 isBskyPostUrl, 16 17 postUriToRelativePath, ··· 722 723 } 723 724 724 725 function getShortenedLength(rt: RichText) { 725 - const {text} = parseMarkdownLinks(rt.text) 726 + const {text} = parseMarkdownRichText(rt.text) 726 727 const newRt = new RichText({text}) 727 728 newRt.detectFacetsWithoutResolution() 728 729 return shortenLinks(newRt).graphemeLength
+2
src/view/com/composer/text-input/TextInput.web.tsx
··· 38 38 import {type AutocompleteRef, createSuggestion} from './web/Autocomplete' 39 39 import {type Emoji} from './web/EmojiPicker' 40 40 import {LinkDecorator} from './web/LinkDecorator' 41 + import {RichTextDecorator} from './web/RichTextDecorator' 41 42 import {TagDecorator} from './web/TagDecorator' 42 43 43 44 export function TextInput({ ··· 64 65 const extensions = useMemo( 65 66 () => [ 66 67 Document, 68 + RichTextDecorator, 67 69 LinkDecorator, 68 70 TagDecorator, 69 71 Mention.configure({
+1 -1
src/view/com/composer/text-input/web/LinkDecorator.ts
··· 42 42 const textContent = node.textContent 43 43 44 44 // markdown links [text](url) 45 - const markdownRegex = /\[([^\]]+)\]\s*\(([^)]+)\)/g 45 + const markdownRegex = /(?<!\\)\[([^\]]+)\]\s*\(([^)]+)\)/g 46 46 let markdownMatch 47 47 while ((markdownMatch = markdownRegex.exec(textContent)) !== null) { 48 48 const from = markdownMatch.index
+100
src/view/com/composer/text-input/web/RichTextDecorator.ts
··· 1 + import {Mark} from '@tiptap/core' 2 + import {type Node as ProsemirrorNode} from '@tiptap/pm/model' 3 + import {Plugin, PluginKey} from '@tiptap/pm/state' 4 + import {Decoration, DecorationSet} from '@tiptap/pm/view' 5 + 6 + import {parseMarkdownEvents} from '#/lib/strings/richtext-markdown' 7 + 8 + function getDecorations(doc: ProsemirrorNode) { 9 + const decorations: Decoration[] = [] 10 + 11 + doc.descendants((node, pos) => { 12 + if (!node.isText || !node.text) return 13 + 14 + const text = node.textContent 15 + const events = parseMarkdownEvents(text) 16 + if (!events) return 17 + 18 + const strongKindStack: ('bold' | 'underline')[] = [] 19 + let awaitingStrongKind = false 20 + 21 + for (const [type, token] of events) { 22 + if (type === 'exit') { 23 + if (token.type === 'strong') strongKindStack.pop() 24 + continue 25 + } 26 + 27 + const from = pos + token.start.offset 28 + const to = pos + token.end.offset 29 + 30 + if (token.type === 'strong') { 31 + awaitingStrongKind = true 32 + } else if ( 33 + token.type === 'emphasisSequence' || 34 + token.type === 'strongSequence' 35 + ) { 36 + if (awaitingStrongKind && token.type === 'strongSequence') { 37 + strongKindStack.push( 38 + text[token.start.offset] === '_' ? 'underline' : 'bold', 39 + ) 40 + awaitingStrongKind = false 41 + } 42 + if (from !== to) 43 + decorations.push( 44 + Decoration.inline(from, to, {class: 'format-delimiter'}), 45 + ) 46 + } else if (from !== to) { 47 + if (token.type === 'emphasisText') { 48 + decorations.push( 49 + Decoration.inline(from, to, {class: 'format-italic'}), 50 + ) 51 + } else if (token.type === 'strongText') { 52 + const kind = strongKindStack[strongKindStack.length - 1] ?? 'bold' 53 + decorations.push( 54 + Decoration.inline(from, to, { 55 + class: kind === 'underline' ? 'format-underline' : 'format-bold', 56 + }), 57 + ) 58 + } else if (token.type === 'characterEscape') { 59 + decorations.push( 60 + Decoration.inline(from, from + 1, {class: 'format-delimiter'}), 61 + ) 62 + } 63 + } 64 + } 65 + }) 66 + 67 + return DecorationSet.create(doc, decorations) 68 + } 69 + 70 + const richTextDecoratorPlugin: Plugin = new Plugin({ 71 + key: new PluginKey('rich-text-decorator'), 72 + 73 + state: { 74 + init: (_, {doc}) => getDecorations(doc), 75 + apply: (transaction, decorationSet) => { 76 + if (transaction.docChanged) { 77 + return getDecorations(transaction.doc) 78 + } 79 + return decorationSet.map(transaction.mapping, transaction.doc) 80 + }, 81 + }, 82 + 83 + props: { 84 + decorations(state) { 85 + return richTextDecoratorPlugin.getState(state) 86 + }, 87 + }, 88 + }) 89 + 90 + export const RichTextDecorator = Mark.create({ 91 + name: 'rich-text-decorator', 92 + priority: 1000, 93 + keepOnSplit: false, 94 + inclusive() { 95 + return true 96 + }, 97 + addProseMirrorPlugins() { 98 + return [richTextDecoratorPlugin] 99 + }, 100 + })
+217 -2
yarn.lock
··· 7580 7580 resolved "https://registry.yarnpkg.com/@types/culori/-/culori-4.0.1.tgz#39ed095e0ef7107342d9091b1707ae8fb8681297" 7581 7581 integrity sha512-43M51r/22CjhbOXyGT361GZ9vncSVQ39u62x5eJdBQFviI8zWp2X5jzqg7k4M6PVgDQAClpy2bUe2dtwEgEDVQ== 7582 7582 7583 + "@types/debug@^4.0.0": 7584 + version "4.1.12" 7585 + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" 7586 + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== 7587 + dependencies: 7588 + "@types/ms" "*" 7589 + 7583 7590 "@types/elliptic@^6.4.9": 7584 7591 version "6.4.18" 7585 7592 resolved "https://registry.yarnpkg.com/@types/elliptic/-/elliptic-6.4.18.tgz#bc96e26e1ccccbabe8b6f0e409c85898635482e1" ··· 7778 7785 version "5.1.2" 7779 7786 resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca" 7780 7787 integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== 7788 + 7789 + "@types/ms@*": 7790 + version "2.1.0" 7791 + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-2.1.0.tgz#052aa67a48eccc4309d7f0191b7e41434b90bb78" 7792 + integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== 7781 7793 7782 7794 "@types/node@*": 7783 7795 version "20.5.1" ··· 9596 9608 resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-2.0.1.tgz#6dafdb25f9d3349914079f010ba8d0e6ff9cd01e" 9597 9609 integrity sha512-oSvEeo6ZUD7NepqAat3RqoucZ5SeqLJgOvVIwkafu6IP3V0pO38s/ypdVUmDDK6qIIHNlYHJAKX9E7R7HoKElw== 9598 9610 9611 + character-entities@^2.0.0: 9612 + version "2.0.2" 9613 + resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" 9614 + integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== 9615 + 9599 9616 chokidar@3.5.1: 9600 9617 version "3.5.1" 9601 9618 resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" ··· 10355 10372 dependencies: 10356 10373 ms "^2.1.1" 10357 10374 10358 - debug@^4.2.0, debug@^4.4.1, debug@^4.4.3: 10375 + debug@^4.0.0, debug@^4.2.0, debug@^4.4.1, debug@^4.4.3: 10359 10376 version "4.4.3" 10360 10377 resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.3.tgz#c6ae432d9bd9662582fce08709b038c58e9e3d6a" 10361 10378 integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== ··· 10390 10407 version "10.6.0" 10391 10408 resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.6.0.tgz#e649a43e3ab953a72192ff5983865e509f37ed9a" 10392 10409 integrity sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== 10410 + 10411 + decode-named-character-reference@^1.0.0: 10412 + version "1.3.0" 10413 + resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz#3e40603760874c2e5867691b599d73a7da25b53f" 10414 + integrity sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q== 10415 + dependencies: 10416 + character-entities "^2.0.0" 10393 10417 10394 10418 decode-uri-component@^0.2.2: 10395 10419 version "0.2.2" ··· 10515 10539 resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 10516 10540 integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== 10517 10541 10518 - dequal@^2.0.3: 10542 + dequal@^2.0.0, dequal@^2.0.3: 10519 10543 version "2.0.3" 10520 10544 resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" 10521 10545 integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== ··· 10549 10573 version "2.1.0" 10550 10574 resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" 10551 10575 integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== 10576 + 10577 + devlop@^1.0.0: 10578 + version "1.1.0" 10579 + resolved "https://registry.yarnpkg.com/devlop/-/devlop-1.1.0.tgz#4db7c2ca4dc6e0e834c30be70c94bbc976dc7018" 10580 + integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA== 10581 + dependencies: 10582 + dequal "^2.0.0" 10552 10583 10553 10584 diff-sequences@^29.6.3: 10554 10585 version "29.6.3" ··· 15312 15343 throat "^5.0.0" 15313 15344 ws "^7.5.10" 15314 15345 yargs "^17.6.2" 15346 + 15347 + micromark-core-commonmark@^2.0.0: 15348 + version "2.0.3" 15349 + resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz#c691630e485021a68cf28dbc2b2ca27ebf678cd4" 15350 + integrity sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg== 15351 + dependencies: 15352 + decode-named-character-reference "^1.0.0" 15353 + devlop "^1.0.0" 15354 + micromark-factory-destination "^2.0.0" 15355 + micromark-factory-label "^2.0.0" 15356 + micromark-factory-space "^2.0.0" 15357 + micromark-factory-title "^2.0.0" 15358 + micromark-factory-whitespace "^2.0.0" 15359 + micromark-util-character "^2.0.0" 15360 + micromark-util-chunked "^2.0.0" 15361 + micromark-util-classify-character "^2.0.0" 15362 + micromark-util-html-tag-name "^2.0.0" 15363 + micromark-util-normalize-identifier "^2.0.0" 15364 + micromark-util-resolve-all "^2.0.0" 15365 + micromark-util-subtokenize "^2.0.0" 15366 + micromark-util-symbol "^2.0.0" 15367 + micromark-util-types "^2.0.0" 15368 + 15369 + micromark-factory-destination@^2.0.0: 15370 + version "2.0.1" 15371 + resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz#8fef8e0f7081f0474fbdd92deb50c990a0264639" 15372 + integrity sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA== 15373 + dependencies: 15374 + micromark-util-character "^2.0.0" 15375 + micromark-util-symbol "^2.0.0" 15376 + micromark-util-types "^2.0.0" 15377 + 15378 + micromark-factory-label@^2.0.0: 15379 + version "2.0.1" 15380 + resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz#5267efa97f1e5254efc7f20b459a38cb21058ba1" 15381 + integrity sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg== 15382 + dependencies: 15383 + devlop "^1.0.0" 15384 + micromark-util-character "^2.0.0" 15385 + micromark-util-symbol "^2.0.0" 15386 + micromark-util-types "^2.0.0" 15387 + 15388 + micromark-factory-space@^2.0.0: 15389 + version "2.0.1" 15390 + resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz#36d0212e962b2b3121f8525fc7a3c7c029f334fc" 15391 + integrity sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg== 15392 + dependencies: 15393 + micromark-util-character "^2.0.0" 15394 + micromark-util-types "^2.0.0" 15395 + 15396 + micromark-factory-title@^2.0.0: 15397 + version "2.0.1" 15398 + resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz#237e4aa5d58a95863f01032d9ee9b090f1de6e94" 15399 + integrity sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw== 15400 + dependencies: 15401 + micromark-factory-space "^2.0.0" 15402 + micromark-util-character "^2.0.0" 15403 + micromark-util-symbol "^2.0.0" 15404 + micromark-util-types "^2.0.0" 15405 + 15406 + micromark-factory-whitespace@^2.0.0: 15407 + version "2.0.1" 15408 + resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz#06b26b2983c4d27bfcc657b33e25134d4868b0b1" 15409 + integrity sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ== 15410 + dependencies: 15411 + micromark-factory-space "^2.0.0" 15412 + micromark-util-character "^2.0.0" 15413 + micromark-util-symbol "^2.0.0" 15414 + micromark-util-types "^2.0.0" 15415 + 15416 + micromark-util-character@^2.0.0: 15417 + version "2.1.1" 15418 + resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-2.1.1.tgz#2f987831a40d4c510ac261e89852c4e9703ccda6" 15419 + integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q== 15420 + dependencies: 15421 + micromark-util-symbol "^2.0.0" 15422 + micromark-util-types "^2.0.0" 15423 + 15424 + micromark-util-chunked@^2.0.0: 15425 + version "2.0.1" 15426 + resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz#47fbcd93471a3fccab86cff03847fc3552db1051" 15427 + integrity sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA== 15428 + dependencies: 15429 + micromark-util-symbol "^2.0.0" 15430 + 15431 + micromark-util-classify-character@^2.0.0: 15432 + version "2.0.1" 15433 + resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz#d399faf9c45ca14c8b4be98b1ea481bced87b629" 15434 + integrity sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q== 15435 + dependencies: 15436 + micromark-util-character "^2.0.0" 15437 + micromark-util-symbol "^2.0.0" 15438 + micromark-util-types "^2.0.0" 15439 + 15440 + micromark-util-combine-extensions@^2.0.0: 15441 + version "2.0.1" 15442 + resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz#2a0f490ab08bff5cc2fd5eec6dd0ca04f89b30a9" 15443 + integrity sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg== 15444 + dependencies: 15445 + micromark-util-chunked "^2.0.0" 15446 + micromark-util-types "^2.0.0" 15447 + 15448 + micromark-util-decode-numeric-character-reference@^2.0.0: 15449 + version "2.0.2" 15450 + resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz#fcf15b660979388e6f118cdb6bf7d79d73d26fe5" 15451 + integrity sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw== 15452 + dependencies: 15453 + micromark-util-symbol "^2.0.0" 15454 + 15455 + micromark-util-encode@^2.0.0: 15456 + version "2.0.1" 15457 + resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz#0d51d1c095551cfaac368326963cf55f15f540b8" 15458 + integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw== 15459 + 15460 + micromark-util-html-tag-name@^2.0.0: 15461 + version "2.0.1" 15462 + resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz#e40403096481986b41c106627f98f72d4d10b825" 15463 + integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA== 15464 + 15465 + micromark-util-normalize-identifier@^2.0.0: 15466 + version "2.0.1" 15467 + resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz#c30d77b2e832acf6526f8bf1aa47bc9c9438c16d" 15468 + integrity sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q== 15469 + dependencies: 15470 + micromark-util-symbol "^2.0.0" 15471 + 15472 + micromark-util-resolve-all@^2.0.0: 15473 + version "2.0.1" 15474 + resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz#e1a2d62cdd237230a2ae11839027b19381e31e8b" 15475 + integrity sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg== 15476 + dependencies: 15477 + micromark-util-types "^2.0.0" 15478 + 15479 + micromark-util-sanitize-uri@^2.0.0: 15480 + version "2.0.1" 15481 + resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz#ab89789b818a58752b73d6b55238621b7faa8fd7" 15482 + integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ== 15483 + dependencies: 15484 + micromark-util-character "^2.0.0" 15485 + micromark-util-encode "^2.0.0" 15486 + micromark-util-symbol "^2.0.0" 15487 + 15488 + micromark-util-subtokenize@^2.0.0: 15489 + version "2.1.0" 15490 + resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz#d8ade5ba0f3197a1cf6a2999fbbfe6357a1a19ee" 15491 + integrity sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA== 15492 + dependencies: 15493 + devlop "^1.0.0" 15494 + micromark-util-chunked "^2.0.0" 15495 + micromark-util-symbol "^2.0.0" 15496 + micromark-util-types "^2.0.0" 15497 + 15498 + micromark-util-symbol@^2.0.0: 15499 + version "2.0.1" 15500 + resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz#e5da494e8eb2b071a0d08fb34f6cefec6c0a19b8" 15501 + integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q== 15502 + 15503 + micromark-util-types@^2.0.0: 15504 + version "2.0.2" 15505 + resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e" 15506 + integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== 15507 + 15508 + micromark@^4.0.2: 15509 + version "4.0.2" 15510 + resolved "https://registry.yarnpkg.com/micromark/-/micromark-4.0.2.tgz#91395a3e1884a198e62116e33c9c568e39936fdb" 15511 + integrity sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA== 15512 + dependencies: 15513 + "@types/debug" "^4.0.0" 15514 + debug "^4.0.0" 15515 + decode-named-character-reference "^1.0.0" 15516 + devlop "^1.0.0" 15517 + micromark-core-commonmark "^2.0.0" 15518 + micromark-factory-space "^2.0.0" 15519 + micromark-util-character "^2.0.0" 15520 + micromark-util-chunked "^2.0.0" 15521 + micromark-util-combine-extensions "^2.0.0" 15522 + micromark-util-decode-numeric-character-reference "^2.0.0" 15523 + micromark-util-encode "^2.0.0" 15524 + micromark-util-normalize-identifier "^2.0.0" 15525 + micromark-util-resolve-all "^2.0.0" 15526 + micromark-util-sanitize-uri "^2.0.0" 15527 + micromark-util-subtokenize "^2.0.0" 15528 + micromark-util-symbol "^2.0.0" 15529 + micromark-util-types "^2.0.0" 15315 15530 15316 15531 micromatch@4.0.5, micromatch@^4.0.2, micromatch@^4.0.4: 15317 15532 version "4.0.5"