diff --git a/README.md b/README.md index 04a9d71..8803f82 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,9 @@ Join the Bibliogram discussion room on Matrix: [#bibliogram:matrix.org](https:// - [x] Galleries of videos - [x] Optimised for mobile - [x] Instance list +- [x] Clickable usernames and hashtags +- [x] Proper error checking - [ ] Image disk cache -- [ ] Clickable usernames and hashtags -- [ ] Proper error checking - [ ] Favicon - [ ] Settings (e.g. data saving) - [ ] List view @@ -33,7 +33,7 @@ Join the Bibliogram discussion room on Matrix: [#bibliogram:matrix.org](https:// - [ ] Public API - [ ] Explore hashtags - [ ] Explore locations -- [ ] _more..._ +- [ ] _more...?_ These features may not be able to be implemented for technical reasons: diff --git a/src/lib/constants.js b/src/lib/constants.js index b02e46a..ede850e 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -42,7 +42,8 @@ let constants = { shortcode_query_hash: "2b0673e0dc4580674a88d426fe00ea90", timeline_fetch_first: 12, username_regex: "[\\w.]+", - shortcode_regex: "[\\w-]+" + shortcode_regex: "[\\w-]+", + hashtag_regex: "[\\w]+" }, resources: { diff --git a/src/lib/structures/TimelineEntry.js b/src/lib/structures/TimelineEntry.js index 6839e54..e311599 100644 --- a/src/lib/structures/TimelineEntry.js +++ b/src/lib/structures/TimelineEntry.js @@ -2,6 +2,7 @@ const constants = require("../constants") const {proxyImage, proxyExtendedOwner} = require("../utils/proxyurl") const {compile} = require("pug") const collectors = require("../collectors") +const {structure} = require("../utils/structuretext") const TimelineBaseMethods = require("./TimelineBaseMethods") const TimelineChild = require("./TimelineChild") require("../testimports")(collectors, TimelineChild, TimelineBaseMethods) @@ -87,6 +88,12 @@ class TimelineEntry extends TimelineBaseMethods { else return edge.node.text.replace(/\u2063/g, "") // I don't know why U+2063 INVISIBLE SEPARATOR is in here, but it is, and it causes rendering issues with certain fonts, so let's just remove it. } + getStructuredCaption() { + const caption = this.getCaption() + if (!caption) return null // no caption + else return structure(caption) + } + /** * Try to get the first meaningful line or sentence from the caption. */ diff --git a/src/lib/structures/User.js b/src/lib/structures/User.js index 5e1d8fe..71d31a2 100644 --- a/src/lib/structures/User.js +++ b/src/lib/structures/User.js @@ -1,5 +1,6 @@ const constants = require("../constants") const {proxyImage} = require("../utils/proxyurl") +const {structure} = require("../utils/structuretext") const Timeline = require("./Timeline") require("../testimports")(constants, Timeline) @@ -17,6 +18,11 @@ class User { this.proxyProfilePicture = proxyImage(this.data.profile_pic_url) } + getStructuredBio() { + if (!this.data.biography) return null + return structure(this.data.biography) + } + getTtl(scale = 1) { const expiresAt = this.cachedAt + constants.caching.resource_cache_time const ttl = expiresAt - Date.now() diff --git a/src/lib/utils/structuretext.js b/src/lib/utils/structuretext.js new file mode 100644 index 0000000..0fecff6 --- /dev/null +++ b/src/lib/utils/structuretext.js @@ -0,0 +1,53 @@ +const constants = require("../constants") +const {Parser} = require("./parser/parser") + +function tryMatch(text, against, callback) { + let matched = text.match(against) + if (matched) callback(matched) +} + +function textToParts(text) { + return [{type: "text", text: text}] +} + +function replacePart(parts, index, match, replacements) { + const toReplace = parts.splice(index, 1)[0] + const before = toReplace.text.slice(0, match.index) + const after = toReplace.text.slice(match.index + match[0].length) + parts.splice(index, 0, ...textToParts(before), ...replacements, ...textToParts(after)) +} + +function partsUsername(parts) { + for (let i = 0; i < parts.length; i++) { + if (parts[i].type === "text") { + tryMatch(parts[i].text, `@(${constants.external.username_regex})`, match => { + replacePart(parts, i, match, [ + {type: "user", text: match[0], user: match[1]} + ]) + i += 1 // skip parts: user + }) + } + } +} + +function partsHashtag(parts) { + for (let i = 0; i < parts.length; i++) { + if (parts[i].type === "text") { + tryMatch(parts[i].text, `#(${constants.external.hashtag_regex})`, match => { + replacePart(parts, i, match, [ + {type: "hashtag", text: match[0], hashtag: match[1]} + ]) + i += 1 // skip parts: hashtag + }) + } + } +} + +function structure(text) { + const parts = textToParts(text) + partsUsername(parts) + partsHashtag(parts) + return parts +} + +module.exports.structure = structure diff --git a/src/site/pug/includes/display_structured.pug b/src/site/pug/includes/display_structured.pug new file mode 100644 index 0000000..22ca62e --- /dev/null +++ b/src/site/pug/includes/display_structured.pug @@ -0,0 +1,11 @@ +mixin display_structured(parts) + each part in parts + if part.type === "text" + = part.text + else if part.type === "user" + a(href="/u/"+part.user).link-to-user= part.text + else if part.type === "hashtag" + //- todo: add link to explore page, when explore page exists. + a.link-to-hashtag= part.text + else + | [UNKNOWN PART TYPE #{part.type}, TEXT:] [#{part.text}] diff --git a/src/site/pug/post.pug b/src/site/pug/post.pug index 4bd8986..2cfd9e1 100644 --- a/src/site/pug/post.pug +++ b/src/site/pug/post.pug @@ -1,3 +1,5 @@ +include includes/display_structured + - const numberFormat = new Intl.NumberFormat().format doctype html @@ -20,7 +22,8 @@ html img(src=post.ownerPfpCacheP width=150 height=150 alt="").pfp a.name(href=`/u/${post.getBasicOwner().username}`)= `${post.data.owner.full_name} (@${post.getBasicOwner().username})` if post.getCaption() - p.description= post.getCaption() + p.structured-text.description + +display_structured(post.getStructuredCaption()) section.images-gallery for entry in post.children if entry.isVideo() diff --git a/src/site/pug/user.pug b/src/site/pug/user.pug index 2b5119d..d69475a 100644 --- a/src/site/pug/user.pug +++ b/src/site/pug/user.pug @@ -2,6 +2,7 @@ include includes/timeline_page.pug include includes/next_page_button.pug +include includes/display_structured - const numberFormat = new Intl.NumberFormat().format @@ -30,7 +31,8 @@ html else h1.full-name= `@${user.data.username}` if !user.fromReel - p.bio= user.data.biography + p.structured-text.bio + +display_structured(user.getStructuredBio()) if user.data.external_url p.website a(href=user.data.external_url)= user.data.external_url diff --git a/src/site/sass/main.sass b/src/site/sass/main.sass index d17d491..94c4799 100644 --- a/src/site/sass/main.sass +++ b/src/site/sass/main.sass @@ -3,6 +3,7 @@ $layout-b-min: 821px $layout-c-max: 680px; $layout-home-a-max: 520px $layout-home-b-min: 521px +$main-theme-link-color: #085cae body margin: 0 @@ -89,7 +90,10 @@ body flex-wrap: wrap justify-content: center - a + a, a:visited + color: $main-theme-link-color + + > * margin: 5px > *:last-child @@ -426,3 +430,14 @@ body .link-list color: $link-color + +.structured-text + a, a:visited + color: $main-theme-link-color + text-decoration: none + + a:link, a:link:visited + text-decoration: underline + + .link-to-hashtag + color: #127722