You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
216 lines
5.7 KiB
216 lines
5.7 KiB
import type { NDKTag } from '@nostr-dev-kit/ndk' |
|
import { last } from 'ramda' |
|
|
|
export const TOPIC = 'topic' |
|
export const LINKCOLLECTION = 'link[]' |
|
export const HTML = 'html' |
|
export const INVOICE = 'invoice' |
|
export const NOSTR_NOTE = 'nostr:note' |
|
export const NOSTR_NEVENT = 'nostr:nevent' |
|
export const NOSTR_NPUB = 'nostr:npub' |
|
export const NOSTR_NPROFILE = 'nostr:nprofile' |
|
export const NOSTR_NADDR = 'nostr:naddr' |
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any |
|
const first = (list: any) => (list ? list[0] : undefined) |
|
|
|
export const fromNostrURI = (s: string) => s.replace(/^[\w+]+:\/?\/?/, '') |
|
|
|
export const urlIsMedia = (url: string): boolean => |
|
(!url.match(/\.(apk|docx|xlsx|csv|dmg)/) && |
|
last(url.split('://'))?.includes('/')) || |
|
false |
|
|
|
export const isImage = (url: string) => |
|
url.match(/^.*\.(jpg|jpeg|png|webp|gif|avif|svg)/gi) |
|
export const isVideo = (url: string) => |
|
url.match(/^.*\.(mov|mkv|mp4|avi|m4v|webm)/gi) |
|
export const isAudio = (url: string) => url.match(/^.*\.(ogg|mp3|wav)/gi) |
|
|
|
export const NEWLINE = 'newline' |
|
type PartTypeNewLine = 'newline' |
|
export type ParsedNewLine = { |
|
type: PartTypeNewLine |
|
value: string |
|
} |
|
|
|
export const LINK = 'link' |
|
type PartTypeLink = 'link' |
|
export type ParsedLink = { |
|
type: PartTypeLink |
|
url: string |
|
is_media: boolean |
|
imeta: Imeta | undefined |
|
} |
|
type Imeta = { |
|
url: string |
|
m: string | undefined |
|
alt: string | undefined |
|
size: string | undefined |
|
dim: string | undefined |
|
x: string | undefined |
|
fallback: string[] |
|
blurhash: string | undefined |
|
} |
|
|
|
export const TEXT = 'text' |
|
type PartTypeText = 'text' |
|
export type ParsedText = { |
|
type: PartTypeText |
|
value: string |
|
} |
|
|
|
export type ParsedPart = ParsedNewLine | ParsedText | ParsedLink |
|
|
|
export const isParsedNewLine = (part: ParsedPart): part is ParsedNewLine => |
|
part.type == NEWLINE |
|
|
|
export const isParsedLink = (part: ParsedPart): part is ParsedLink => |
|
part.type == LINK |
|
|
|
export const isParsedText = (part: ParsedPart): part is ParsedText => |
|
part.type == TEXT |
|
|
|
export const parseContent = (content: string, tags: NDKTag[]): ParsedPart[] => { |
|
const result: ParsedPart[] = [] |
|
let text = content.trim() |
|
let buffer = '' |
|
|
|
const getIMeta = (url: string): undefined | Imeta => { |
|
const imeta_tag_for_url = tags.find( |
|
(tag) => tag[0] === 'imeta' && tag.some((e) => e.includes(url)) |
|
) |
|
if (!imeta_tag_for_url) return undefined |
|
const pairs = imeta_tag_for_url.map((s) => [ |
|
s.split(' ')[0], |
|
s.substring(s.indexOf(' ') + 1), |
|
]) |
|
return { |
|
url, |
|
m: pairs.find((p) => p[0] === 'm')?.[1], |
|
alt: pairs.find((p) => p[0] === 'alt')?.[1], |
|
x: pairs.find((p) => p[0] === 'x')?.[1], |
|
size: pairs.find((p) => p[0] === 'size')?.[1], |
|
dim: pairs.find((p) => p[0] === 'dim')?.[1], |
|
blurhash: pairs.find((p) => p[0] === 'blurhash')?.[1], |
|
fallback: pairs.filter((p) => p[0] === 'fallback')?.map((p) => p[1]), |
|
} |
|
} |
|
|
|
const parseNewline = (): undefined | [string, ParsedNewLine] => { |
|
const newline: string = first(text.match(/^\n+/)) |
|
|
|
if (newline) { |
|
return [newline, { type: NEWLINE, value: newline }] |
|
} |
|
} |
|
|
|
const parseUrl = (): undefined | [string, ParsedLink] => { |
|
const raw: string = first( |
|
text.match( |
|
/^([a-z\+:]{2,30}:\/\/)?[^<>\(\)\s]+\.[a-z]{2,6}[^\s]*[^<>"'\.!?,:\s\)\(]/gi |
|
) |
|
) |
|
|
|
// Skip url if it's just the end of a filepath |
|
if (!raw) { |
|
return |
|
} |
|
|
|
const prev = last(result) |
|
|
|
if (prev?.type === TEXT && prev.value.endsWith('/')) { |
|
return |
|
} |
|
|
|
let url = raw |
|
|
|
// Skip ellipses and very short non-urls |
|
if (url.match(/\.\./)) { |
|
return |
|
} |
|
|
|
if (!url.match('://')) { |
|
url = 'https://' + url |
|
} |
|
|
|
return [ |
|
raw, |
|
{ type: LINK, url, is_media: urlIsMedia(url), imeta: getIMeta(url) }, |
|
] |
|
} |
|
|
|
while (text) { |
|
// The order that this runs matters |
|
const part = parseNewline() || parseUrl() |
|
|
|
if (part) { |
|
if (buffer) { |
|
result.push({ type: TEXT, value: buffer }) |
|
buffer = '' |
|
} |
|
|
|
const [raw, parsed] = part |
|
|
|
result.push(parsed) |
|
text = text.slice(raw.length) |
|
} else { |
|
// Instead of going character by character and re-running all the above regular expressions |
|
// a million times, try to match the next word and add it to the buffer |
|
const match = first(text.match(/^[\w\d]+ ?/i)) || text[0] |
|
|
|
buffer += match |
|
text = text.slice(match.length) |
|
} |
|
} |
|
|
|
if (buffer) { |
|
result.push({ type: TEXT, value: buffer }) |
|
} |
|
|
|
return result |
|
} |
|
|
|
export const isCoverLetter = (s: string): boolean => { |
|
return s.indexOf('PATCH 0/') > 0 |
|
} |
|
|
|
/** this doesn't work for all patch formats and options */ |
|
export const extractPatchMessage = (s: string): string | undefined => { |
|
try { |
|
if (isCoverLetter(s)) { |
|
return s.substring(s.indexOf('] ') + 2) |
|
} |
|
const t = s.split('\nSubject: [')[1].split('] ')[1] |
|
if (t.split('\n\n---\n ').length > 1) return t.split('\n\n---\n ')[0] |
|
return t.split('\n\ndiff --git ')[0].split('\n\n ').slice(0, -1).join('') |
|
} catch { |
|
return undefined |
|
} |
|
} |
|
|
|
/** this doesn't work for all patch formats and options */ |
|
export const extractPatchTitle = (s: string): string | undefined => { |
|
const msg = extractPatchMessage(s) |
|
if (!msg) return undefined |
|
return s.split('\n')[0] |
|
} |
|
|
|
/** patch message without first line */ |
|
export const extractPatchDescription = (s: string): string | undefined => { |
|
const msg = extractPatchMessage(s) |
|
if (!msg) return '' |
|
const i = msg.indexOf('\n') |
|
if (i === -1) return '' |
|
return msg.substring(i).trim() |
|
} |
|
|
|
export const extractIssueTitle = (s: string): string => { |
|
return s.split('\n')[0] || '' |
|
} |
|
|
|
export const extractIssueDescription = (s: string): string => { |
|
const split = s.split('\n') |
|
if (split.length === 0) return '' |
|
return s.substring(split[0].length) || '' |
|
}
|
|
|