mirror of
				https://git.sr.ht/~cadence/bibliogram
				synced 2025-10-31 03:25:36 +00:00 
			
		
		
		
	Clickable usernames and hashtags
Should work well enough. Report edge cases.
This commit is contained in:
		
							parent
							
								
									1fcdfce868
								
							
						
					
					
						commit
						0ea95d1943
					
				| @ -21,9 +21,9 @@ Join the Bibliogram discussion room on Matrix: [#bibliogram:matrix.org](https:// | |||||||
| - [x] Galleries of videos | - [x] Galleries of videos | ||||||
| - [x] Optimised for mobile | - [x] Optimised for mobile | ||||||
| - [x] Instance list | - [x] Instance list | ||||||
|  | - [x] Clickable usernames and hashtags | ||||||
|  | - [x] Proper error checking | ||||||
| - [ ] Image disk cache | - [ ] Image disk cache | ||||||
| - [ ] Clickable usernames and hashtags |  | ||||||
| - [ ] Proper error checking |  | ||||||
| - [ ] Favicon | - [ ] Favicon | ||||||
| - [ ] Settings (e.g. data saving) | - [ ] Settings (e.g. data saving) | ||||||
| - [ ] List view | - [ ] List view | ||||||
| @ -33,7 +33,7 @@ Join the Bibliogram discussion room on Matrix: [#bibliogram:matrix.org](https:// | |||||||
| - [ ] Public API | - [ ] Public API | ||||||
| - [ ] Explore hashtags | - [ ] Explore hashtags | ||||||
| - [ ] Explore locations | - [ ] Explore locations | ||||||
| - [ ] _more..._ | - [ ] _more...?_ | ||||||
| 
 | 
 | ||||||
| These features may not be able to be implemented for technical reasons: | These features may not be able to be implemented for technical reasons: | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -42,7 +42,8 @@ let constants = { | |||||||
| 		shortcode_query_hash: "2b0673e0dc4580674a88d426fe00ea90", | 		shortcode_query_hash: "2b0673e0dc4580674a88d426fe00ea90", | ||||||
| 		timeline_fetch_first: 12, | 		timeline_fetch_first: 12, | ||||||
| 		username_regex: "[\\w.]+", | 		username_regex: "[\\w.]+", | ||||||
| 		shortcode_regex: "[\\w-]+" | 		shortcode_regex: "[\\w-]+", | ||||||
|  | 		hashtag_regex: "[\\w]+" | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	resources: { | 	resources: { | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ const constants = require("../constants") | |||||||
| const {proxyImage, proxyExtendedOwner} = require("../utils/proxyurl") | const {proxyImage, proxyExtendedOwner} = require("../utils/proxyurl") | ||||||
| const {compile} = require("pug") | const {compile} = require("pug") | ||||||
| const collectors = require("../collectors") | const collectors = require("../collectors") | ||||||
|  | const {structure} = require("../utils/structuretext") | ||||||
| const TimelineBaseMethods = require("./TimelineBaseMethods") | const TimelineBaseMethods = require("./TimelineBaseMethods") | ||||||
| const TimelineChild = require("./TimelineChild") | const TimelineChild = require("./TimelineChild") | ||||||
| require("../testimports")(collectors, TimelineChild, TimelineBaseMethods) | 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.
 | 		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. | 	 * Try to get the first meaningful line or sentence from the caption. | ||||||
| 	 */ | 	 */ | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| const constants = require("../constants") | const constants = require("../constants") | ||||||
| const {proxyImage} = require("../utils/proxyurl") | const {proxyImage} = require("../utils/proxyurl") | ||||||
|  | const {structure} = require("../utils/structuretext") | ||||||
| const Timeline = require("./Timeline") | const Timeline = require("./Timeline") | ||||||
| require("../testimports")(constants, Timeline) | require("../testimports")(constants, Timeline) | ||||||
| 
 | 
 | ||||||
| @ -17,6 +18,11 @@ class User { | |||||||
| 		this.proxyProfilePicture = proxyImage(this.data.profile_pic_url) | 		this.proxyProfilePicture = proxyImage(this.data.profile_pic_url) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	getStructuredBio() { | ||||||
|  | 		if (!this.data.biography) return null | ||||||
|  | 		return structure(this.data.biography) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	getTtl(scale = 1) { | 	getTtl(scale = 1) { | ||||||
| 		const expiresAt = this.cachedAt + constants.caching.resource_cache_time | 		const expiresAt = this.cachedAt + constants.caching.resource_cache_time | ||||||
| 		const ttl = expiresAt - Date.now() | 		const ttl = expiresAt - Date.now() | ||||||
|  | |||||||
							
								
								
									
										53
									
								
								src/lib/utils/structuretext.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/lib/utils/structuretext.js
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
							
								
								
									
										11
									
								
								src/site/pug/includes/display_structured.pug
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/site/pug/includes/display_structured.pug
									
									
									
									
									
										Normal file
									
								
							| @ -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}] | ||||||
| @ -1,3 +1,5 @@ | |||||||
|  | include includes/display_structured | ||||||
|  | 
 | ||||||
| - const numberFormat = new Intl.NumberFormat().format | - const numberFormat = new Intl.NumberFormat().format | ||||||
| 
 | 
 | ||||||
| doctype html | doctype html | ||||||
| @ -20,7 +22,8 @@ html | |||||||
| 					img(src=post.ownerPfpCacheP width=150 height=150 alt="").pfp | 					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})` | 					a.name(href=`/u/${post.getBasicOwner().username}`)= `${post.data.owner.full_name} (@${post.getBasicOwner().username})` | ||||||
| 				if post.getCaption() | 				if post.getCaption() | ||||||
| 					p.description= post.getCaption() | 					p.structured-text.description | ||||||
|  | 						+display_structured(post.getStructuredCaption()) | ||||||
| 			section.images-gallery | 			section.images-gallery | ||||||
| 				for entry in post.children | 				for entry in post.children | ||||||
| 					if entry.isVideo() | 					if entry.isVideo() | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ | |||||||
| 
 | 
 | ||||||
| include includes/timeline_page.pug | include includes/timeline_page.pug | ||||||
| include includes/next_page_button.pug | include includes/next_page_button.pug | ||||||
|  | include includes/display_structured | ||||||
| 
 | 
 | ||||||
| - const numberFormat = new Intl.NumberFormat().format | - const numberFormat = new Intl.NumberFormat().format | ||||||
| 
 | 
 | ||||||
| @ -30,7 +31,8 @@ html | |||||||
| 					else | 					else | ||||||
| 						h1.full-name= `@${user.data.username}` | 						h1.full-name= `@${user.data.username}` | ||||||
| 					if !user.fromReel | 					if !user.fromReel | ||||||
| 						p.bio= user.data.biography | 						p.structured-text.bio | ||||||
|  | 							+display_structured(user.getStructuredBio()) | ||||||
| 						if user.data.external_url | 						if user.data.external_url | ||||||
| 							p.website | 							p.website | ||||||
| 								a(href=user.data.external_url)= user.data.external_url | 								a(href=user.data.external_url)= user.data.external_url | ||||||
|  | |||||||
| @ -3,6 +3,7 @@ $layout-b-min: 821px | |||||||
| $layout-c-max: 680px; | $layout-c-max: 680px; | ||||||
| $layout-home-a-max: 520px | $layout-home-a-max: 520px | ||||||
| $layout-home-b-min: 521px | $layout-home-b-min: 521px | ||||||
|  | $main-theme-link-color: #085cae | ||||||
| 
 | 
 | ||||||
| body | body | ||||||
| 	margin: 0 | 	margin: 0 | ||||||
| @ -89,7 +90,10 @@ body | |||||||
| 			flex-wrap: wrap | 			flex-wrap: wrap | ||||||
| 			justify-content: center | 			justify-content: center | ||||||
| 
 | 
 | ||||||
| 			a | 			a, a:visited | ||||||
|  | 				color: $main-theme-link-color | ||||||
|  | 
 | ||||||
|  | 			> * | ||||||
| 				margin: 5px | 				margin: 5px | ||||||
| 
 | 
 | ||||||
| 		> *:last-child | 		> *:last-child | ||||||
| @ -426,3 +430,14 @@ body | |||||||
| 
 | 
 | ||||||
| 			.link-list | 			.link-list | ||||||
| 				color: $link-color | 				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 | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user