diff --git a/src/lib/constants.js b/src/lib/constants.js index 4941e86..e2c7e63 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -112,6 +112,11 @@ let constants = { default: "", boolean: true, replaceEmptyWithDefault: true + },{ + name: "remove_trailing_hashtags", + default: "", + boolean: true, + replaceEmptyWithDefault: false },{ name: "link_hashtags", default: "", @@ -122,6 +127,11 @@ let constants = { default: "on", boolean: true, replaceEmptyWithDefault: false + },{ + name: "infinite_scroll", + default: "normal", + boolean: false, + replaceEmptyWithDefault: true },{ name: "caption_side", default: "left", @@ -263,7 +273,7 @@ let constants = { additional_routes: [], - database_version: 7 + database_version: 8 } // Override values from config and export the result diff --git a/src/lib/structures/TimelineEntry.js b/src/lib/structures/TimelineEntry.js index eeb8024..8998c2c 100644 --- a/src/lib/structures/TimelineEntry.js +++ b/src/lib/structures/TimelineEntry.js @@ -2,7 +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 {structure, removeTrailingHashtags} = require("../utils/structuretext") const TimelineBaseMethods = require("./TimelineBaseMethods") const TimelineChild = require("./TimelineChild") require("../testimports")(collectors, TimelineChild, TimelineBaseMethods) @@ -110,6 +110,12 @@ class TimelineEntry extends TimelineBaseMethods { else return structure(caption) } + getStructuredCaptionWithoutTrailingHashtags() { + const structured = this.getStructuredCaption() + if (!structured) return null // no caption + else return removeTrailingHashtags(structured) + } + /** * Try to get the first meaningful line or sentence from the caption. */ diff --git a/src/lib/utils/structuretext.js b/src/lib/utils/structuretext.js index f40e746..c38e17a 100644 --- a/src/lib/utils/structuretext.js +++ b/src/lib/utils/structuretext.js @@ -1,5 +1,20 @@ const constants = require("../constants") +const dots = [ + ".", // full stop + "\u00b7", // middle dot + "\u2022", // bullet + "\u2027", // hyphenation point + "\u2219", // bullet operator + "\u22c5", // dot operator + "\u2e31", // word separator middle dot + "\u2e33", // raised dot + "\u30fb", // katakana middle dot + "\uff65", // halfwidth katakana middle dot +] + +const dotRegex = new RegExp(`[\n ][\n #${dots.join("")}]*$`, "gms") + function tryMatch(text, against, callback) { if (against instanceof RegExp && against.global) { // if it's a global match, keep sending matches to the callback while the callback returns true @@ -67,6 +82,41 @@ function structure(text) { return parts } +/** + * Edit a structure in-place to remove trailing hashtags and separator characters. + */ +function removeTrailingHashtags(structured) { + let hasHashtags = structured.some(part => part.type === "hashtag") + let seenHashtags = false + + function shouldRemoveLastPart() { + const part = structured[structured.length-1] + if (part.type === "hashtag") { + seenHashtags = true + return true + } else if (part.type === "user") { + if (hasHashtags && !seenHashtags) { // compromise? + return true + } + } else if (part.type === "text") { + const content = part.text.replace(dotRegex, "") + if (content.length === 0) { + return true + } else { + part.text = content + } + } + return false + } + + while (shouldRemoveLastPart()) { + structured.pop() + } + + return structured +} + module.exports.structure = structure module.exports.partsUsername = partsUsername module.exports.partsHashtag = partsHashtag +module.exports.removeTrailingHashtags = removeTrailingHashtags diff --git a/src/lib/utils/upgradedb.js b/src/lib/utils/upgradedb.js index 91e5546..13295c8 100644 --- a/src/lib/utils/upgradedb.js +++ b/src/lib/utils/upgradedb.js @@ -80,7 +80,7 @@ const deltas = new Map([ db.transaction(() => { db.prepare("ALTER TABLE UserSettings ADD COLUMN save_data TEXT NOT NULL DEFAULT 'automatic'") .run() - db.prepare("ALTER TABLE UserSettings ADD COLUMN rewrite_youtube TEXT NOT NULL DEFAULt ''") + db.prepare("ALTER TABLE UserSettings ADD COLUMN rewrite_youtube TEXT NOT NULL DEFAULT ''") .run() db.prepare("ALTER TABLE UserSettings ADD COLUMN rewrite_twitter TEXT NOT NULL DEFAULT ''") .run() @@ -94,6 +94,15 @@ const deltas = new Map([ db.prepare("CREATE TABLE CSRFTokens (token TEXT NOT NULL, expires INTEGER NOT NULL, PRIMARY KEY (token))") .run() })() + }], + // version 7 to version 8 + [8, function() { + db.transaction(() => { + db.prepare("ALTER TABLE UserSettings ADD COLUMN remove_trailing_hashtags INTEGER NOT NULL DEFAULT 0") + .run() + db.prepare("ALTER TABLE UserSettings ADD COLUMN infinite_scroll TEXT NOT NULL DEFAULT 'normal'") + .run() + })() }] ]) diff --git a/src/site/pug/includes/post.pug b/src/site/pug/includes/post.pug index 961b49b..393d8bd 100644 --- a/src/site/pug/includes/post.pug +++ b/src/site/pug/includes/post.pug @@ -28,7 +28,10 @@ mixin post(post, headerWithNavigation) div if post.getCaption() p.structured-text.description - +display_structured(post.getStructuredCaption()) + if settings.remove_trailing_hashtags + +display_structured(post.getStructuredCaptionWithoutTrailingHashtags()) + else + +display_structured(post.getStructuredCaption()) footer if willDisplayAltInDescription diff --git a/src/site/pug/settings.pug b/src/site/pug/settings.pug index 6aa95de..e2b50e6 100644 --- a/src/site/pug/settings.pug +++ b/src/site/pug/settings.pug @@ -61,12 +61,20 @@ html +input("rewrite_twitter", "Rewrite Twitter domain", "twitter.com", true) - +checkbox("show_comments", "Display comments", "Display", true) + +checkbox("remove_trailing_hashtags", "Hide trailing hashtags", false) +checkbox("link_hashtags", "Clickable hashtags", "Clickable", true) + +checkbox("show_comments", "Display comments", "Display", true) + +checkbox("spa", "Fast navigation", "Enabled", false) + +select("infinite_scroll", "Infinite scroll", true, [ + {value: "normal", text: "Normal"}, + {value: "eager", text: "Eager"}, + {value: "off", text: "Manual"} + ]) + +fieldset("Appearance") +select("theme", "Theme", false, constants.themes.collated.map(entry => ({value: entry.file, text: entry.name}))) diff --git a/test/structuretext.js b/test/structuretext.js index ed31b3b..22fa510 100644 --- a/test/structuretext.js +++ b/test/structuretext.js @@ -1,5 +1,5 @@ const tap = require("tap") -const {structure, partsHashtag, partsUsername} = require("../src/lib/utils/structuretext.js") +const {structure, partsHashtag, partsUsername, removeTrailingHashtags} = require("../src/lib/utils/structuretext.js") // lone test hashtag tap.same( @@ -154,3 +154,91 @@ tap.test("entire structure works", childTest => { childTest.end() }) + +tap.test("remove trailing hashtags", childTest => { + childTest.same( + removeTrailingHashtags(structure( + "Happy earth day folks #flyingfish" + )), + [ + {type: "text", text: "Happy earth day folks"} + ], + "earth day" + ) + + childTest.same( + removeTrailingHashtags(structure( + "šŸŒHELLO OLIVE HEREšŸŒ...and we have been working hard on this magic trick for youuuUuu." + + "\n." + + "\n. ." + + "\n." + + "\n." + + "\n#guineapig #cavy #guineapigs #guineapigsofinstagram #cute #babyanimals #cavylove #babyguineapig #guineapigsof_ig #cavy #thedodo #spoiltpets #funny #pets #guineapigpopcorning #popcorning #guineapigsleeping #vipmischief #tiktok #tiktokmemes" + )), + [ + {type: "text", text: "šŸŒHELLO OLIVE HEREšŸŒ...and we have been working hard on this magic trick for youuuUuu."} + ], + "olive" + ) + + childTest.same( + removeTrailingHashtags(structure( + "PINK HOUSE. ." + + "\n." + + "\n." + + "\n." + + "\n." + + "\n." + + "\n#antireality #archicage #arqsketch #thebna #next_top_architects #architecturedose #architecture_hunter #morpholio #archdaily #designboom #arch_more #designmilk #arch_impressive #designwanted #nextarch #dezeen #amazingarchitecture #koozarch #superarchitects #thearchitecturestudentblog #architecturestudents #architecturefactor #allofarchitecture #archinect #soarch #" + )), + [ + {type: "text", text: "PINK HOUSE."} + ], + "pink house" + ) + + childTest.same( + removeTrailingHashtags(structure( + "This some research Iā€™ve been doing for #FuturePlay at @futuredeluxe together with @curtisbaigent Expressive Computer Vision #1" + )), + [ + {type: "text", text: "This some research Iā€™ve been doing for "}, + {type: "hashtag", text: "#FuturePlay", hashtag: "FuturePlay"}, + {type: "text", text: " at "}, + {type: "user", text: "@futuredeluxe", user: "futuredeluxe"}, + {type: "text", text: " together with "}, + {type: "user", text: "@curtisbaigent", user: "curtisbaigent"}, + {type: "text", text: " Expressive Computer Vision"} + ], + "computer vision" + ) + + childTest.same( + removeTrailingHashtags(structure( + "It is a flourishing building in" + + "\nthe midst of a great bustling city." + + "\nPeople will get out from difficulty," + + "\nand this will be the resurrection time." + + "\n#hellofrom Chongqing China" + + "\n惻" + + "\n惻" + + "\n惻" + + "\n惻" + + "\n#earthfocus #earthoffcial #earthpix #discoverearth #lifeofadventure #livingonearth #theweekoninstagram #theglobewanderer #visualambassadors #welivetoexplore #IamATraveler #wonderful_places #TLPics #depthobsessed #voyaged @sonyalpha @hypebeast @highsnobiety @lightroom @soul.planet @earthfever @9gag @500px" + )), + [ + {type: "text", text: + "It is a flourishing building in" + + "\nthe midst of a great bustling city." + + "\nPeople will get out from difficulty," + + "\nand this will be the resurrection time." + + "\n" + }, + {type: "hashtag", text: "#hellofrom", hashtag: "hellofrom"}, + {type: "text", text: " Chongqing China"} + ], + "chongquing china" + ) + + childTest.end() +})