Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

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

feat: nicer facet parsing

authored by

easrng and committed by
Tangled
32abb8b7 1ae3d814

+190 -125
+2 -1
__tests__/lib/string.test.ts
··· 1 1 import {RichText} from '@atproto/api' 2 2 import {i18n} from '@lingui/core' 3 3 4 + import {detectFacetsWithoutResolution} from '#/lib/strings/detect-facets' 4 5 import {parseEmbedPlayerFromUrl} from '#/lib/strings/embed-player' 5 6 import { 6 7 createStarterPackGooglePlayUri, ··· 327 328 for (let i = 0; i < inputs.length; i++) { 328 329 const input = inputs[i] 329 330 const inputRT = new RichText({text: input}) 330 - inputRT.detectFacetsWithoutResolution() 331 + detectFacetsWithoutResolution(inputRT) 331 332 const outputRT = shortenLinks(inputRT) 332 333 expect(outputRT.text).toEqual(outputs[i][0]) 333 334 expect(outputRT.facets?.length).toEqual(outputs[i][1].length)
+1
package.json
··· 91 91 "@bsky.app/sift": "^0.3.1", 92 92 "@bsky.app/tapper": "^0.5.0", 93 93 "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", 94 + "@easrng/tr58": "0.0.4", 94 95 "@emoji-mart/data": "^1.2.1", 95 96 "@emoji-mart/react": "^1.1.1", 96 97 "@expo/html-elements": "^0.12.5",
+2 -1
src/components/FeedCard.tsx
··· 9 9 import {Plural, Trans, useLingui} from '@lingui/react/macro' 10 10 import {useQueryClient} from '@tanstack/react-query' 11 11 12 + import {detectFacetsWithoutResolution} from '#/lib/strings/detect-facets' 12 13 import {sanitizeHandle} from '#/lib/strings/handles' 13 14 import {logger} from '#/logger' 14 15 import {precacheFeedFromGeneratorView} from '#/state/queries/feed' ··· 213 214 const rt = useMemo(() => { 214 215 if (!description) return 215 216 const rt = new RichTextApi({text: description || ''}) 216 - rt.detectFacetsWithoutResolution() 217 + detectFacetsWithoutResolution(rt) 217 218 return rt 218 219 }, [description]) 219 220 if (!rt) return null
+2 -1
src/components/ProfileCard.tsx
··· 17 17 import {makeProfileLink} from '#/lib/routes/links' 18 18 import {forceLTR} from '#/lib/strings/bidi' 19 19 import {NON_BREAKING_SPACE} from '#/lib/strings/constants' 20 + import {detectFacetsWithoutResolution} from '#/lib/strings/detect-facets' 20 21 import {sanitizeDisplayName} from '#/lib/strings/display-names' 21 22 import {sanitizeHandle} from '#/lib/strings/handles' 22 23 import {useProfileShadow} from '#/state/cache/profile-shadow' ··· 400 401 const rt = useMemo(() => { 401 402 if (!('description' in profile)) return 402 403 const rt = new RichTextApi({text: profile.description || ''}) 403 - rt.detectFacetsWithoutResolution() 404 + detectFacetsWithoutResolution(rt) 404 405 return rt 405 406 }, [profile]) 406 407 if (!rt) return null
+2 -1
src/components/RichText.tsx
··· 2 2 import {type StyleProp, type TextStyle} from 'react-native' 3 3 import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api' 4 4 5 + import {detectFacetsWithoutResolution} from '#/lib/strings/detect-facets' 5 6 import {toShortUrl} from '#/lib/strings/url-helpers' 6 7 import {atoms as a, flatten, type TextStyleProp} from '#/alf' 7 8 import {isOnlyEmoji} from '#/alf/typography' ··· 61 62 return value 62 63 } else { 63 64 const rt = new RichTextAPI({text: value}) 64 - rt.detectFacetsWithoutResolution() 65 + detectFacetsWithoutResolution(rt) 65 66 return rt 66 67 } 67 68 }, [value])
+7 -3
src/components/dialogs/lists/CreateOrEditListDialog.tsx
··· 5 5 import {useLingui} from '@lingui/react' 6 6 import {Plural, Trans} from '@lingui/react/macro' 7 7 8 + import { 9 + detectFacets, 10 + detectFacetsWithoutResolution, 11 + } from '#/lib/strings/detect-facets' 8 12 import {cleanError} from '#/lib/strings/errors' 9 13 import {isOverMaxGraphemeCount} from '#/lib/strings/helpers' 10 14 import {richTextToString} from '#/lib/strings/rich-text-helpers' ··· 169 173 const serialized = richTextToString(new RichTextAPI({text, facets}), false) 170 174 171 175 const richText = new RichTextAPI({text: serialized}) 172 - richText.detectFacetsWithoutResolution() 176 + detectFacetsWithoutResolution(richText) 173 177 174 178 return richText 175 179 }) ··· 227 231 {cleanNewlines: true}, 228 232 ) 229 233 230 - await richText.detectFacets(agent) 234 + await detectFacets(agent, richText) 231 235 richText = shortenLinks(richText) 232 236 richText = stripInvalidMentions(richText) 233 237 ··· 355 359 const onChangeDescription = useCallback( 356 360 (newText: string) => { 357 361 const richText = new RichTextAPI({text: newText}) 358 - richText.detectFacetsWithoutResolution() 362 + detectFacetsWithoutResolution(richText) 359 363 360 364 setDescriptionRt(richText) 361 365 },
+2 -1
src/components/hooks/useRichText.ts
··· 1 1 import {useEffect, useState} from 'react' 2 2 import {RichText as RichTextAPI} from '@atproto/api' 3 3 4 + import {detectFacets} from '#/lib/strings/detect-facets' 4 5 import {useAgent} from '#/state/session' 5 6 6 7 export function useRichText(text: string): [RichTextAPI, boolean] { ··· 19 20 async function resolveRTFacets() { 20 21 // new each time 21 22 const resolvedRT = new RichTextAPI({text}) 22 - await resolvedRT.detectFacets(agent) 23 + await detectFacets(agent, resolvedRT) 23 24 if (!ignore) { 24 25 setResolvedRT(resolvedRT) 25 26 }
+2 -1
src/lib/api/index.ts
··· 45 45 type ThreadDraft, 46 46 } from '#/view/com/composer/state/composer' 47 47 import {createGIFDescription} from '../gif-alt-text' 48 + import {detectFacets} from '../strings/detect-facets' 48 49 import {uploadBlob} from './upload-blob' 49 50 50 51 export {uploadBlob} ··· 215 216 parseMarkdownLinks(trimmedText) 216 217 217 218 let rt = new RichText({text: parsedText}) 218 - await rt.detectFacets(agent) 219 + await detectFacets(agent, rt) 219 220 220 221 if (markdownFacets.length > 0) { 221 222 const nonOverlapping = (rt.facets || []).filter(f => {
+125
src/lib/strings/detect-facets.ts
··· 1 + import {type BskyAgent, type Facet, type RichText} from '@atproto/api' 2 + import {tokenize} from '@easrng/tr58' 3 + 4 + const TAG_CHARS = ['#', '#', '$'] 5 + 6 + export interface FacetRun { 7 + text: string 8 + features: Facet['features'] 9 + } 10 + 11 + export function detectFacetRunsWithoutResolution(text: string): FacetRun[] { 12 + const facetRuns: FacetRun[] = [] 13 + 14 + const tokens = tokenize(text, { 15 + nonStandard: {domainHandle: true, tags: TAG_CHARS}, 16 + }) 17 + 18 + for (const token of tokens) { 19 + if (token.type === 'URL') { 20 + const val = token.value 21 + 22 + // Handle mentions (@handle.com) 23 + if (/^[@@]/.test(token.value)) { 24 + facetRuns.push({ 25 + text: val, 26 + features: [ 27 + { 28 + $type: 'app.bsky.richtext.facet#mention', 29 + did: val.slice(1) as any, 30 + }, 31 + ], 32 + }) 33 + } 34 + // Handle tags (#tag or $cashtag) 35 + else if (TAG_CHARS.some(char => val.startsWith(char))) { 36 + const normalized = (val[0] === '$' ? val : val.slice(1)).normalize( 37 + 'NFKC', 38 + ) 39 + facetRuns.push({ 40 + text: val, 41 + features: /^\$?\d/.test(normalized) 42 + ? [] 43 + : [ 44 + { 45 + $type: 'app.bsky.richtext.facet#tag', 46 + tag: normalized, 47 + }, 48 + ], 49 + }) 50 + } else { 51 + let uri = val 52 + if (!/^[a-z][a-z0-9+.-]*:\/\//.test(uri)) { 53 + uri = `https://${uri}` 54 + } 55 + const NON_EMAIL = /^[^@@]+?([?/#:]|$)/ 56 + facetRuns.push({ 57 + text: val, 58 + // don't link email addresses 59 + features: NON_EMAIL.test(token.value) 60 + ? [ 61 + { 62 + $type: 'app.bsky.richtext.facet#link', 63 + uri: uri, 64 + }, 65 + ] 66 + : [], 67 + }) 68 + } 69 + } else { 70 + facetRuns.push({ 71 + text: token.value, 72 + features: [], 73 + }) 74 + } 75 + } 76 + return facetRuns 77 + } 78 + 79 + export function detectFacetsWithoutResolution(rt: RichText) { 80 + const facets: Facet[] = [] 81 + 82 + let currentByteOffset = 0 83 + 84 + for (const run of detectFacetRunsWithoutResolution(rt.text)) { 85 + const runBytes = new TextEncoder().encode(run.text) 86 + const start = currentByteOffset 87 + const end = start + runBytes.byteLength 88 + 89 + if (run.features.length) { 90 + facets.push({ 91 + index: {byteStart: start, byteEnd: end}, 92 + features: run.features, 93 + }) 94 + } 95 + 96 + currentByteOffset = end 97 + } 98 + 99 + rt.facets = facets 100 + return rt 101 + } 102 + 103 + export async function detectFacets(agent: BskyAgent, rt: RichText) { 104 + detectFacetsWithoutResolution(rt) 105 + if (rt.facets) { 106 + for (const facet of rt.facets) { 107 + for (const feature of facet.features) { 108 + if ( 109 + feature.$type === 'app.bsky.richtext.facet#mention' && 110 + 'did' in feature && 111 + !feature.did.startsWith('did:') 112 + ) { 113 + try { 114 + const res = await agent.resolveHandle({handle: feature.did}) 115 + feature.did = res.data.did 116 + } catch (e) { 117 + facet.features = facet.features.filter(f => f !== feature) 118 + } 119 + } 120 + } 121 + } 122 + } 123 + 124 + return rt 125 + }
+2 -1
src/screens/Messages/components/MessageInputEmbed.tsx
··· 16 16 type CommonNavigatorParams, 17 17 type NavigationProp, 18 18 } from '#/lib/routes/types' 19 + import {detectFacetsWithoutResolution} from '#/lib/strings/detect-facets' 19 20 import { 20 21 convertBskyAppUrlIfNeeded, 21 22 isBskyPostUrl, ··· 76 77 setEmbed: (embedUrl: string | undefined) => void, 77 78 ) { 78 79 const rt = new RichTextAPI({text: message}) 79 - rt.detectFacetsWithoutResolution() 80 + detectFacetsWithoutResolution(rt) 80 81 81 82 let uriFromFacet: string | undefined 82 83
+6 -2
src/screens/Messages/components/MessagesList.tsx
··· 19 19 20 20 import {useHideBottomBarBorderForScreen} from '#/lib/hooks/useHideBottomBarBorder' 21 21 import {ScrollProvider} from '#/lib/ScrollContext' 22 + import { 23 + detectFacets, 24 + detectFacetsWithoutResolution, 25 + } from '#/lib/strings/detect-facets' 22 26 import {shortenLinks, stripInvalidMentions} from '#/lib/strings/rich-text-manip' 23 27 import { 24 28 convertBskyAppUrlIfNeeded, ··· 325 329 // detect facets without resolution first - this is used to see if there's 326 330 // any post links in the text that we can embed. We do this first because 327 331 // we want to remove the post link from the text, re-trim, then detect facets 328 - rt.detectFacetsWithoutResolution() 332 + detectFacetsWithoutResolution(rt) 329 333 330 334 let embed: $Typed<AppBskyEmbedRecord.Main> | undefined 331 335 ··· 379 383 } 380 384 } 381 385 382 - await rt.detectFacets(agent) 386 + await detectFacets(agent, rt) 383 387 384 388 rt = shortenLinks(rt) 385 389 rt = stripInvalidMentions(rt)
+3 -2
src/state/queries/starter-packs.ts
··· 18 18 19 19 import {until} from '#/lib/async/until' 20 20 import {createStarterPackList} from '#/lib/generate-starterpack' 21 + import {detectFacets} from '#/lib/strings/detect-facets' 21 22 import { 22 23 createStarterPackUri, 23 24 httpStarterPackUriToAtUri, ··· 116 117 let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined 117 118 if (description) { 118 119 const rt = new RichText({text: description}) 119 - await rt.detectFacets(agent) 120 + await detectFacets(agent, rt) 120 121 descriptionFacets = rt.facets 121 122 } 122 123 ··· 188 189 let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined 189 190 if (description) { 190 191 const rt = new RichText({text: description}) 191 - await rt.detectFacets(agent) 192 + await detectFacets(agent, rt) 192 193 descriptionFacets = rt.facets 193 194 } 194 195
+2 -1
src/view/com/composer/drafts/state/api.ts
··· 8 8 import {getDeviceName} from '#/lib/deviceName' 9 9 import {getImageDim} from '#/lib/media/manip' 10 10 import {mimeToExt} from '#/lib/media/video/util' 11 + import {detectFacetsWithoutResolution} from '#/lib/strings/detect-facets' 11 12 import {shortenLinks} from '#/lib/strings/rich-text-manip' 12 13 import {type ComposerImage} from '#/state/gallery' 13 14 import {type Gif} from '#/state/queries/tenor' ··· 420 421 const posts = await Promise.all( 421 422 draft.posts.map(async (post, index) => { 422 423 const richtext = new RichText({text: post.text || ''}) 423 - richtext.detectFacetsWithoutResolution() 424 + detectFacetsWithoutResolution(richtext) 424 425 425 426 const embed: EmbedDraft = { 426 427 quote: undefined,
+5 -4
src/view/com/composer/state/composer.ts
··· 9 9 import {nanoid} from 'nanoid/non-secure' 10 10 11 11 import {type SelfLabel} from '#/lib/moderation' 12 + import {detectFacetsWithoutResolution} from '#/lib/strings/detect-facets' 12 13 import {insertMentionAt} from '#/lib/strings/mention-manip' 13 14 import {parseMarkdownLinks, shortenLinks} from '#/lib/strings/rich-text-manip' 14 15 import { ··· 685 686 * we suggest at most 1 of each. 686 687 */ 687 688 if (initText) { 688 - initRichText.detectFacetsWithoutResolution() 689 + detectFacetsWithoutResolution(initRichText) 689 690 const detectedExtUris = new Map<string, LinkFacetMatch>() 690 691 const detectedPostUris = new Map<string, LinkFacetMatch>() 691 692 if (initRichText.facets) { ··· 734 735 } 735 736 } else if (initMention) { 736 737 // highlight the mention 737 - initRichText.detectFacetsWithoutResolution() 738 + detectFacetsWithoutResolution(initRichText) 738 739 } 739 740 740 741 return { ··· 772 773 function getShortenedLength(rt: RichText) { 773 774 const {text} = parseMarkdownLinks(rt.text) 774 775 const newRt = new RichText({text}) 775 - newRt.detectFacetsWithoutResolution() 776 + detectFacetsWithoutResolution(newRt) 776 777 return shortenLinks(newRt).graphemeLength 777 778 } 778 779 ··· 785 786 }, 786 787 ): PostDraft { 787 788 const richtext = new RichText({text}) 788 - richtext.detectFacetsWithoutResolution() 789 + detectFacetsWithoutResolution(richtext) 789 790 790 791 return { 791 792 id: overrides?.id ?? nanoid(),
+2 -1
src/view/com/composer/text-input/TextInput.tsx
··· 19 19 import {POST_IMG_MAX} from '#/lib/constants' 20 20 import {downloadAndResize} from '#/lib/media/manip' 21 21 import {isUriImage} from '#/lib/media/util' 22 + import {detectFacetsWithoutResolution} from '#/lib/strings/detect-facets' 22 23 import {getMentionAt, insertMentionAt} from '#/lib/strings/mention-manip' 23 24 import {useTheme} from '#/lib/ThemeContext' 24 25 import { ··· 110 111 } 111 112 112 113 const newRt = new RichText({text: newText}) 113 - newRt.detectFacetsWithoutResolution() 114 + detectFacetsWithoutResolution(newRt) 114 115 115 116 const markdownFacets: AppBskyRichtextFacet.Main[] = [] 116 117 const regex = /\[([^\]]+)\]\s*\(([^)]+)\)/g
+2 -1
src/view/com/composer/text-input/TextInput.web.tsx
··· 24 24 25 25 import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' 26 26 import {blobToDataUri, isUriImage} from '#/lib/media/util' 27 + import {detectFacetsWithoutResolution} from '#/lib/strings/detect-facets' 27 28 import {useActorAutocompleteFn} from '#/state/queries/actor-autocomplete' 28 29 import { 29 30 type LinkFacetMatch, ··· 290 291 const isPaste = window.event?.type === 'paste' 291 292 292 293 const newRt = new RichText({text: newText}) 293 - newRt.detectFacetsWithoutResolution() 294 + detectFacetsWithoutResolution(newRt) 294 295 295 296 const markdownFacets: AppBskyRichtextFacet.Main[] = [] 296 297 const regex = /\[([^\]]+)\]\s*\(([^)]+)\)/g
-39
src/view/com/composer/text-input/web/LinkDecorator.ts
··· 14 14 * the facet-set. 15 15 */ 16 16 17 - import {URL_REGEX} from '@atproto/api' 18 17 import {Mark} from '@tiptap/core' 19 18 import {type Node as ProsemirrorNode} from '@tiptap/pm/model' 20 19 import {Plugin, PluginKey} from '@tiptap/pm/state' 21 20 import {Decoration, DecorationSet} from '@tiptap/pm/view' 22 - 23 - import {isValidDomain} from '#/lib/strings/url-helpers' 24 21 25 22 export const LinkDecorator = Mark.create({ 26 23 name: 'link-decorator', ··· 53 50 }), 54 51 ) 55 52 } 56 - 57 - // regular links 58 - iterateUris(textContent, (from, to) => { 59 - decorations.push( 60 - Decoration.inline(pos + from, pos + to, { 61 - class: 'autolink', 62 - }), 63 - ) 64 - }) 65 53 } 66 54 }) 67 55 ··· 90 78 }) 91 79 return linkDecoratorPlugin 92 80 } 93 - 94 - function iterateUris(str: string, cb: (from: number, to: number) => void) { 95 - let match 96 - const re = URL_REGEX 97 - while ((match = re.exec(str))) { 98 - let uri = match[2] 99 - if (!uri.startsWith('http')) { 100 - const domain = match.groups?.domain 101 - if (!domain || !isValidDomain(domain)) { 102 - continue 103 - } 104 - uri = `https://${uri}` 105 - } 106 - let from = str.indexOf(match[2], match.index) 107 - let to = from + match[2].length 108 - // strip ending puncuation 109 - if (/[.,;!?]$/.test(uri)) { 110 - uri = uri.slice(0, -1) 111 - to-- 112 - } 113 - if (/[)]$/.test(uri) && !uri.includes('(')) { 114 - uri = uri.slice(0, -1) 115 - to-- 116 - } 117 - cb(from, to) 118 - } 119 - }
+16 -64
src/view/com/composer/text-input/web/TagDecorator.ts
··· 1 - /** 2 - * TipTap is a stateful rich-text editor, which is extremely useful 3 - * when you _want_ it to be stateful formatting such as bold and italics. 4 - * 5 - * However we also use "stateless" behaviors, specifically for URLs 6 - * where the text itself drives the formatting. 7 - * 8 - * This plugin uses a regex to detect URIs and then applies 9 - * link decorations (a <span> with the "autolink") class. That avoids 10 - * adding any stateful formatting to TipTap's document model. 11 - * 12 - * We then run the URI detection again when constructing the 13 - * RichText object from TipTap's output and merge their features into 14 - * the facet-set. 15 - */ 16 - 17 - import { 18 - CASHTAG_REGEX, 19 - TAG_REGEX, 20 - TRAILING_PUNCTUATION_REGEX, 21 - } from '@atproto/api' 22 1 import {Mark} from '@tiptap/core' 23 2 import {type Node as ProsemirrorNode} from '@tiptap/pm/model' 24 3 import {Plugin, PluginKey} from '@tiptap/pm/state' 25 4 import {Decoration, DecorationSet} from '@tiptap/pm/view' 5 + 6 + import {detectFacetRunsWithoutResolution} from '#/lib/strings/detect-facets' 26 7 27 8 function getDecorations(doc: ProsemirrorNode) { 28 9 const decorations: Decoration[] = [] 29 10 30 11 doc.descendants((node, pos) => { 31 12 if (node.isText && node.text) { 32 - const regex = TAG_REGEX 33 13 const textContent = node.textContent 34 - 35 - // Detect hashtags 36 - let match 37 - while ((match = regex.exec(textContent))) { 38 - const [matchedString, __, tag] = match 39 - 40 - if (!tag || tag.replace(TRAILING_PUNCTUATION_REGEX, '').length > 64) 41 - continue 42 - 43 - const [trailingPunc = ''] = tag.match(TRAILING_PUNCTUATION_REGEX) || [] 44 - const matchedFrom = match.index + matchedString.indexOf(tag) 45 - const matchedTo = matchedFrom + (tag.length - trailingPunc.length) 46 - 47 - /* 48 - * The match is exclusive of `#` so we need to adjust the start of the 49 - * highlight by -1 to include the `#` 50 - */ 51 - const start = pos + matchedFrom - 1 52 - const end = pos + matchedTo 53 - 54 - decorations.push( 55 - Decoration.inline(start, end, { 56 - class: 'autolink', 57 - }), 58 - ) 59 - } 60 - 61 - // Detect cashtags 62 - const cashtagRegex = new RegExp(CASHTAG_REGEX.source, 'gu') 63 - while ((match = cashtagRegex.exec(textContent))) { 64 - const [_fullMatch, leading, ticker] = match 14 + const facetRuns = detectFacetRunsWithoutResolution(textContent) 65 15 66 - if (!ticker) continue 16 + let currentOffset = 0 17 + for (const run of facetRuns) { 18 + const runLength = run.text.length 67 19 68 - // Calculate positions: leading char + $ + ticker 69 - const matchedFrom = match.index + leading.length 70 - const matchedTo = matchedFrom + 1 + ticker.length // +1 for $ 20 + if (run.features?.length) { 21 + const start = pos + currentOffset 22 + const end = start + runLength 71 23 72 - const start = pos + matchedFrom 73 - const end = pos + matchedTo 24 + decorations.push( 25 + Decoration.inline(start, end, { 26 + class: 'autolink', 27 + }), 28 + ) 29 + } 74 30 75 - decorations.push( 76 - Decoration.inline(start, end, { 77 - class: 'autolink', 78 - }), 79 - ) 31 + currentOffset += runLength 80 32 } 81 33 } 82 34 })
+2 -1
src/view/screens/Profile.tsx
··· 22 22 type NativeStackScreenProps, 23 23 type NavigationProp, 24 24 } from '#/lib/routes/types' 25 + import {detectFacets} from '#/lib/strings/detect-facets' 25 26 import {combinedDisplayName} from '#/lib/strings/display-names' 26 27 import {cleanError} from '#/lib/strings/errors' 27 28 import {isInvalidHandle} from '#/lib/strings/handles' ··· 616 617 async function resolveRTFacets() { 617 618 // new each time 618 619 const resolvedRT = new RichTextAPI({text}) 619 - await resolvedRT.detectFacets(agent) 620 + await detectFacets(agent, resolvedRT) 620 621 if (!ignore) { 621 622 setResolvedRT(resolvedRT) 622 623 }
+5
yarn.lock
··· 2650 2650 resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" 2651 2651 integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== 2652 2652 2653 + "@easrng/tr58@0.0.4": 2654 + version "0.0.4" 2655 + resolved "https://registry.yarnpkg.com/@easrng/tr58/-/tr58-0.0.4.tgz#920492370f6312c797c8abb353a3a16ff875901b" 2656 + integrity sha512-PnUOpYeKCN9vjKgP0YWOVwvGv4F3Aw/v1sKomY1w4Sci7nltJ3yvbKFVKUF9t7eljdrMukuzMGndCUnFiqArhg== 2657 + 2653 2658 "@egjs/hammerjs@^2.0.17": 2654 2659 version "2.0.17" 2655 2660 resolved "https://registry.yarnpkg.com/@egjs/hammerjs/-/hammerjs-2.0.17.tgz#5dc02af75a6a06e4c2db0202cae38c9263895124"