bluesky client without react native baggage written in sveltekit
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342let MARKUP_TYPE = "com.example.richtext.facet#markup"let r = String.rawlet whitespace = r`\s\u00AD\u2060\u200A\u200B\u200C\u200D\u20e2`let url = r`[-\w/%&=#+~@$*'!?,.;:]*`let url_final = r`[-\w/%&=#+~@$*']`let big_regex = RegExp([ r`\b(?<link>https?://${url}${url_final}([(]${url}[)](${url}${url_final})?)?)(?<link_open>\[)?`, // alternative less strict link regex: //r`\b(?<link>(https?://|([a-zA-Z0-9-]+[.])+[a-zA-Z0-9-]{2,}(?=[/]))${url}${url_final}([(]${url}[)](${url}${url_final})?)?)(?<link_open>\[)?`, // urls must either have a scheme, or have a slash after the domain, to be considered. // ie we will never link common "word.word" mistakes. r`(?<=^|\s)[##](?<hashtag>(?!\uFE0F)[^${whitespace}]*[^\p{P}${whitespace}])`, // note: must filter out #123 r`(?<=^|\s|[(])@(?<mention>[-a-zA-Z0-9]+([.][-a-zA-Z0-9]+)+)\b`, r`(?<style>[*][*]|[_][_]|[~][~]|[/])`, r`(?<close>\])`, r`(?<open>\[)`, r`\x60(?<code>.*?)\x60`, r`[\\]facet(?<facet>\[.*?\])\n\[`, r`[\\](?<tag_open>[a-z]+)\[`, r`[\\](?<escaped>.)`,].join("|"), 'gu')const INITIALS = /(https?:[/][/])|#|#|@|[*][*]|[_][_]|[~][~]|[/]|\]|\[|`|[\\]/g const STYLE_SURROUND = { __proto__: null, italic: Object.freeze(["/","/"]), bold: Object.freeze(["**","**"]), underline: Object.freeze(["__","__"]), strikethrough: Object.freeze(["~~","~~"]), code: Object.freeze(["`","`"]),}const TAG_FEATURES = { __proto__: null, i: Object.freeze({$type:MARKUP_TYPE, style:'italic', strip:STYLE_SURROUND['italic']}), b: Object.freeze({$type:MARKUP_TYPE, style:'bold', strip:STYLE_SURROUND['bold']}), u: Object.freeze({$type:MARKUP_TYPE, style:'underline', strip:STYLE_SURROUND['underline']}), s: Object.freeze({$type:MARKUP_TYPE, style:'strikethrough', strip:STYLE_SURROUND['strikethrough']}), code: Object.freeze({$type:MARKUP_TYPE, style:'code', strip:STYLE_SURROUND['code']}),}
const STYLE_START = /^[\s,][^\s,]|^['"}{(>|\[][^\s,'"]/const STYLE_END = /^[^\s,][-\s.,:;!?'"}{)<\\|\]]/const ITALIC_START = /^[\s,][^\s,/]|^['"}{(|\[][^\s,'"/<]/const ITALIC_END = /^[^\s,/>][-\s.,:;!?'"}{)\\|\]]/const STYLE_SYNTAX = { __proto__:null, '**': 'bold', '__': 'underline', '~~': 'strikethrough', '/': 'italic',}function check_style(match, open_styles) { let type = STYLE_SYNTAX[match.groups.style] let before = match.input.charAt(match.index-1)||"\n" let after = match.input.charAt(match.index+match[0].length)||"\n" let feature = open_styles.get(type) let side = 'none' if (feature) { if (('italic'==type ? ITALIC_END : STYLE_END).test(before+after)) side = 'close' } else { if (('italic'==type ? ITALIC_START : STYLE_START).test(before+after)) side = 'open' } return {side, type, feature}}
function finalize_overlay_span({text, features, owner}) { let cn = "" for (let f of features) if (!f._cancelled) cn += " "+f.$type.split("#")[1] if (owner) { if (owner._cancelled) // if a syntax span has its owner feature cancelled, we still want to highlight that // so the user knows there /would/ be something here if they had closed the tag etc. cn += " cancelled" cn += " "+owner.$type.split("#")[1]+"-syntax" } return [text, cn]}
function parse_facet_json(text) { try { let json = JSON.parse(text) if (!(json instanceof Array)) return null for (let feature of json) { if ('string'!=typeof feature.$type) return null } return json } catch(e) { } return null}
function richtext_to_markup({text, facets}) { text = text.replace(INITIALS, "\\$&") // hm can't we just pass big_regex to this? // TODO: try to convert faceted text back into a markup string, for post editing // this is a difficult task, and will often be impossible. in that case, we have a raw facet tag // but ideally, we should try to use regular syntax as much as possible. return text}
function highlight(text) { // facet system let events = [] // [event] let open_features = new Map() // feature → event let open_styles = new Map() // type → feature let open_brackets = [] // [feature] // overlay system let overlay_last = 0 let overlay_spans = [] function overlay(end, feature=null) { if (end > overlay_last) { let text2 = text.slice(overlay_last, end) overlay_spans.push({text:text2, features:new Set(open_features.keys()), owner:feature}) overlay_last = end } } // events function event_text(text) { events.push({type:'text', feature:null, text}) } function event_open(feature, oncancel="") { let event = {type:'open', feature, text:oncancel} events.push(event) open_features.set(feature, event) } function event_close(feature, oncancel="") { let event = {type:'close', feature, text:oncancel} events.push(event) open_features.delete(feature) } function event_cancel(feature) { let event = open_features.get(feature) open_features.delete(feature) event.type = 'text' event.feature = null feature._cancelled = true // eghh.. (this is ok because these are only used for overlay spans now) } let last = 0 for (let match of text.matchAll(big_regex)) { let start = match.index let end = start + match[0].length let g = match.groups if (start > last) event_text(text.slice(last, start)) last = end if (g.mention!=null) { let feature = {$type:"app.bsky.richtext.facet#mention", did:g.mention} overlay(start) event_open(feature) overlay(end, feature) event_text(match[0]) event_close(feature) } else if (g.link!=null) { let feature = {$type:"app.bsky.richtext.facet#link", uri:g.link} overlay(start) if (g.link_open!=null) { overlay(end, feature) event_open(feature, match[0]) open_brackets.push(feature) } else { event_open(feature) overlay(end, feature) event_text(g.link) event_close(feature) } } else if (g.hashtag!=null && !/^\d+$/.test(g.hashtag)) { let feature = {$type:"app.bsky.richtext.facet#tag", tag:g.hashtag} overlay(start) event_open(feature) overlay(end, feature) event_text(match[0]) event_close(feature) } else if (g.style!=null) { let {side, type, feature} = check_style(match, open_styles) if ('open'==side) { feature = {$type:MARKUP_TYPE, style:type, strip:STYLE_SURROUND[type]} overlay(start) overlay(end, feature) open_styles.set(type, feature) event_open(feature, match[0]) } else if ('close'==side) { overlay(start) event_close(feature, match[0]) overlay(end, feature) open_styles.delete(type) } else { event_text(match[0]) } } else if (g.open!=null) { event_text(match[0]) open_brackets.push(null) } else if (g.close!=null && open_brackets.length) { let feature = open_brackets.pop() if (feature) { overlay(start) if (feature instanceof Array) { for (let f of feature) event_close(f) // nn overlay(end, {$type:"#multi"}) } else { event_close(feature, match[0]) overlay(end, feature) } } else { event_text(match[0]) } } else if (g.code!=null) { let feature = {$type:MARKUP_TYPE, style:'code', strip:STYLE_SURROUND['code']} overlay(start) overlay(end, feature) event_open(feature, "`") event_text(g.code) event_close(feature, "`") } else if (g.tag_open!=null) { let feature = TAG_FEATURES[g.tag_open] if (feature) { feature = {...feature} overlay(start) overlay(end, feature) event_open(feature, match[0]) open_brackets.push(feature) } else { event_text(match[0]) } } else if (g.escaped!=null) { let feature = {$type:"#escape"} // only used for overlay overlay(start) event_text(g.escaped) overlay(end, feature) } else if (g.facet!=null) { let features = parse_facet_json(g.facet) if (features) { overlay(start) overlay(end, {$type: "#facet"}) for (let feature of features) event_open(feature) open_brackets.push(features) } else { overlay(start) overlay(end, {$type: "#facet", _cancelled:true}) event_text("<invalid>[") open_brackets.push(null) } } else { overlay(end) event_text(match[0]) } } if (text.length > last) event_text(text.slice(last, text.length)) overlay(text.length) for (let feature of open_features.keys()) event_cancel(feature) //console.log(events, overlay_spans) return { overlay_spans: overlay_spans.map((span)=>finalize_overlay_span(span)), data: events_to_richtext(events), }}
function has_strip(feature) { return MARKUP_TYPE==feature.$type}
// note: do not reuse feature objectsfunction events_to_richtext(events) { let text = "" let facets = [] let open_features = new Set() let start = 0 function flush_facet() { if (text.length > start) { facets.push({ index: {start, end:text.length}, features: [...open_features].map(feature=>{ if (has_strip(feature)) { // convert _surround into strip let feat = {...feature, strip:[0,0]} // copy if (start == feature.strip.start) // at start feat.strip[0] = feature.strip.surround[0].length if (text.length == feature.strip.end) { // at end feat.strip[1] = feature.strip.surround[1].length } else { //feat.cont = true // todo: for the renderer: maybe continuation should be the default if the strips are both 0? // anyway, which side of the gap should this flag be on? should it be a continued forward flag, or a merge backward flag? // merge is probably easier for rendering (or actually do we want it to be a "don't merge, start a new block" flag?) // ugh this one is annoying because like we dont have merge flags for builtin facets.. so like, // maybe we can try to store that information elsewhere? or idk. // 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.. } feature = feat } return feature }), }) start = text.length } } for (let event of events) { if ('text'==event.type) { text += event.text } if ('open'==event.type) { flush_facet() open_features.add(event.feature) if (has_strip(event.feature)) { event.feature.strip = {surround:event.feature.strip, start: text.length, end: null} text += event.feature.strip.surround[0] } } if ('close'==event.type) { if (has_strip(event.feature)) { text += event.feature.strip.surround[1] event.feature.strip.end = text.length } flush_facet() open_features.delete(event.feature) } } return {text, facets}}