cohost descriptive page titles

adds descriptive, dynamically updating titles to cohost pages, replacing the default "cohost!" for everything

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         cohost descriptive page titles
// @namespace    https://github.com/adrianmgg
// @version      1.0.4
// @description  adds descriptive, dynamically updating titles to cohost pages, replacing the default "cohost!" for everything
// @author       amgg
// @match        https://cohost.org/*
// @icon         https://cohost.org/static/a4f72033a674e35d4cc9.png
// @grant        unsafeWindow
// @run-at       document-start
// @compatible   firefox
// @compatible   chrome
// @license      MIT
// ==/UserScript==

const rules = [
    [['pathname', '^/$', 'home']],
    [['pathname', '^/rc/project/following/?$', 'following']],
    [['pathname', '^/rc/project/notifications/?$', 'notifications']],
    [['pathname', '^/rc/tagged/(.*)$', '#$1']],
    [['pathname', '^/rc/search$', 'search: '], ['search', '^(.*)$', (match) => new URLSearchParams(match[1]).get('q') ]],
    [['pathname', '^/([^/]+)/?$', '@$1']],
    [['pathname', '^/[^/]+/posts/unpublished/?$', 'drafts']],
    [['pathname', '^/rc/project/followers/?$', 'followers']],
    [['pathname', '^/[^/]+/follow-requests/?$', 'follow requests']],
    [['pathname', '^/rc/user/settings/?$', 'settings']],
    [['pathname', '^/[^/]+/post/compose/?$', 'compose post']], // cohost does actually already have a different title for this one, "cohost - go ahead, make a post"
    [['pathname', '^/[^/]+/post/[^/]+/edit/?', 'edit post' ]],
    // readyState can be as early as `loading` in func, might need to let these be promises rather than executing immediately
    // TODO this gives "$display_name on cohost" for posts without titles, maybe do something else for that case?
    [['pathname', '^/[^/]+/post/[^/]+/?', () => document.head.querySelector('meta[property="og:title"]').content ]],
    [['pathname', '^/rc/content/tos/?$', 'terms of use']],
    [['pathname', '^/rc/content/privacy/?$', 'privacy notice']],
    [['pathname', '^/rc/content/community-guidelines/?$', 'community guidelines']],
    [['pathname', '^/rc/content/markdown-reference/?$', 'markdown cheatsheet']],
    // [['pathname', '^/rc/welcome/?$', '']], // just gonna leave this one with the default "cohost!"
    [['pathname', '^/rc/signup/?$', 'sign up']],
    [['pathname', '^/rc/project/edit/?$', 'edit profile']],
].map(rule => rule.map(([type, test, replacement]) => [type, new RegExp(test), replacement]) );
function location_to_title(location) {
    for(const rule of rules) {
        if(rule.every(([type, regex,]) => regex.test(location[type]))) {
            return rule.map(([type, regex, thing]) => {
                if(typeof thing === 'function') return thing(regex.exec(location[type]));
                else return location[type].replace(regex, thing);
            }).join('');
        }
    }
    return null;
}


// ======== ========

let expected_title = null;
let prev_location = null;
function title_needs_update() {
    // don't try to update the title if we haven't found the title node yet.
    if(title_node === null) return;
    // if url has changed, re-compute expected title
    if(prev_location !== unsafeWindow.location.href) {
        expected_title = location_to_title(unsafeWindow.location) ?? 'cohost!';
        prev_location = unsafeWindow.location.href;
    }
    // if current title doesn't match expected title, set it
    if(title_node.text !== expected_title) {
        title_node.text = expected_title;
    }
}


// ======== watching for title changes ========

function observer_chain_thing(child_node_name, child_observer_func) { // TODO filter function instead of child name?
    return (target) => {
        const observer = new MutationObserver((mutations, observer) => {
            for(const mutation of mutations) {
                if(mutation.type === 'childList') {
                    for(const node of mutation.addedNodes) {
                        if(node.nodeType === Node.ELEMENT_NODE && node.nodeName === child_node_name) {
                            child_observer_func(node);
                        }
                    }
                }
            }
        });
        for(const node of target.childNodes) {
            if(node.nodeType === Node.ELEMENT_NODE && node.nodeName === child_node_name) {
                child_observer_func(node);
            }
        }
        observer.observe(target, { childList: true });
    }
}

let title_node = null;
// this'll stop observing stuff if document.documentElement gets swapped out under our noses but i think it's probably safe to assume that won't happen
observer_chain_thing(
    'HEAD',
    observer_chain_thing(
        'TITLE',
        (node) => {
            title_node = node;
            const observer = new MutationObserver((mutations, observer) => {
                title_needs_update();
            });
            // `childList: true` is for when text nodes are added/removed,
            //  `characterData: true, subtree: true` is for when existing text nodes have their data changed
            observer.observe(node, { childList: true, characterData: true, subtree: true });
            // also call this once right away since this new title node probably came with a new title
            title_needs_update(node);
        },
    ),
)(document.documentElement);


// ======== watching for url changes ========

const History__replaceState = History.prototype.replaceState;
History.prototype.replaceState = function() {
    const ret = History__replaceState.apply(this, arguments);
    title_needs_update();
    return ret;
};