generated at
リンクにアイコンを付けるCSSを生成するスクリプト
以下のUserCSSを自動生成する


サンプル
今のところSteamDBのみ

注意点
依存しないようにした

スクリプト
CSSを生成するには、パソコンから以下のコマンドを実行する
$ deno run --allow-run --reload https://scrapbox.io/api/code/Mijinko/リンクにアイコンを付けるCSSを生成するスクリプト/script.ts
Denoを導入していないと実行できないので注意
--allow-runiuioiua/deno_copy_pasteクリップボードにコピーするモジュール)が要求しているため必要
実行後はクリップボードにCSSが保存されるので、それをUserCSSとして適用する

script.ts
script.ts
/// <reference no-default-lib="true" /> /// <reference lib="esnext" /> import { ChildStyle, Style, toCSSText, writeText } from "./deps.ts"; import { generateExternalLinkStyle, generateLinkIconStyle, LinkInit, } from "./stylesheetGenerator.ts"; /** * CSSの生成に使用するオブジェクト */ const externalLinks: LinkInit[] = [{ // PDF url: { url: ".pdf", selectorSyntax: "$=", }, style: { "content": "'\\f1c1'" }, }, { // GitHub url: [ "https://github.com", "https://raw.githubusercontent.com", "https://gist.github.com", ], style: { "content": "'\\f09b'" }, }, { // Twitter url: [ "https://twitter.com", "https://pbs.twimg.com", "https://video.twimg.com", ], style: { "content": "'\\f099'" }, }, { // Wikipedia url: [ "https://ja.wikipedia.org", "https://en.wikipedia.org", ], style: { "content": "'\\f266'" }, }, { // Amazon url: [ "https://www.amazon.co.jp", "https://amazon.co.jp", ], style: { "content": "'\\f270'" }, }, { // Discord url: [ "https://discord.com", "https://discord.app", "https://discordapp.com", "https://discordstatus.com/", ], style: { "content": "'\\f392'" }, }, { // Cloudflare url: [ "https://www.cloudflare.com", "https://cdnjs.cloudflare.com", ], style: { "content": "'\\e07d'" }, }, { // Steam url: [ "https://store.steampowered.com", "https://help.steampowered.com", "https://steamcommunity.com", "https://s.team", ], style: { "content": "'\\f1b6'" }, }, { // Steam外部サービス url: [ // SteamDB "https://steamdb.info", "https://steamstat.us", ], style: { "content": "'\\f3f6'" }, }, { // Googleドライブ url: [ "https://drive.google.com", ], style: { "content": "'\\f3aa'" }, }]; const cssImport = '@import "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css";'; const fontFamily: ChildStyle = { "font-family": "'Font Awesome 6 Free', 'Font Awesome 6 Brands'", }; const elStyle: Style = generateExternalLinkStyle(externalLinks, { icon: fontFamily, }); const liStyleBase: ChildStyle = { "font-weight": 400, "margin-right": "1px", }; const liStyle: Style = generateLinkIconStyle(externalLinks, { iconBase: { ...fontFamily, ...liStyleBase }, }); const extraStyle: Style = { // 標準機能のTwitterアイコンを非表示にする "i.fa-twitter::before": { "content": "none", }, ".line .link .favicon.fa-twitter": { "margin": 0, }, }; await writeText( cssImport + toCSSText({ ...elStyle, ...liStyle, ...extraStyle }), );

stylesheetGenerator.ts
stylesheetGenerator.ts
/// <reference no-default-lib="true" /> /// <reference lib="esnext" /> import { ChildStyle, Style } from "./deps.ts"; export type AttributeSelectorOperator = "^=" | "=" | "$=" | "*=" | "~=" | "|="; export interface URLInit { url: string; /** * 属性セレクタに使用する演算子 * 指定しない場合は`^=`が用いられる */ selectorSyntax: AttributeSelectorOperator; } export interface LinkInit { url: string | URLInit | (string | URLInit)[]; style: ChildStyle; } /** * 外部リンクを区別するUserCSSを生成する関数 */ export function generateExternalLinkStyle( /** アイコンを付与しないURLが入ったオブジェクト */ links: LinkInit[], /** オプションで設定するスタイル(CSS) */ style?: { /** 外部リンクを区別するアイコンに設定するスタイル */ icon?: ChildStyle; }, ): Style { const urls = getURLFromLinkInit(links); const selector = ".line span:not(.modal-image):not(.pointing-device-map) > a.link:not(.icon):not(:is(" + urls.reduce( (pre, crt) => `${pre}[href${crt.selectorSyntax}"${crt.url}"],`, "", ) + "))::after"; const iconStyleDefault: ChildStyle = { "display": "inline-block", "padding-right": "2px", "font-size": ".5em", "font-weight": "bold", "font-family": "'Font Awesome 5 Free', 'Font Awesome 5 Brands'", "content": "'\\f35d'", "cursor": "text", }; const iconStyle: Style = { [selector]: style?.icon === undefined ? iconStyleDefault : { ...iconStyleDefault, ...style.icon }, }; return iconStyle; } /** * 特定のリンクにアイコンをつけるUserCSSを生成する関数 */ export function generateLinkIconStyle( /** アイコンを付与するリンクの情報 */ links: LinkInit[], /** オプションで設定するスタイル(CSS) */ style?: { /** アイコンに共通で設定するスタイル */ iconBase?: ChildStyle; }, ): Style { const allURLs = getURLFromLinkInit(links); const baseSelector = ".line span:not(.deco-\\.) > span > a.link:is(" + allURLs.reduce( (pre, crt) => `${pre}[href${crt.selectorSyntax}"${crt.url}"],`, "", ) + ")::before"; const baseStyleDefault: ChildStyle = { "display": "inline-block", "width": "auto", "height": "1em", "line-height": "1em", "vertical-align": "middle", "text-align": "center", "background-size": "contain", "background-repeat": "no-repeat", "font-family": "'Font Awesome 5 Free', 'Font Awesome 5 Brands'", "cursor": "text", }; /** 先頭にアイコンを付けるための共通のスタイル */ const baseStyle: Style = { [baseSelector]: style?.iconBase === undefined ? baseStyleDefault : { ...baseStyleDefault, ...style.iconBase }, }; const individualStyle: Style = {}; for (const link of links) { const urls = getURLFromLinkInit([link]); const selector = ":is(.line, .line .deco) a.link:is(" + urls.reduce( (pre, crt) => `${pre}[href${crt.selectorSyntax}"${crt.url}"],`, "", ) + ")::before"; individualStyle[selector] = link.style; } return { ...baseStyle, ...individualStyle }; } /** * LinkInitの配列からURLを取り出す * @param {LinkInit[]} links 取り出し元のオブジェクト * @param {AttributeSelectorOperator} [defaultOperator="^="] selectorSyntaxが指定されていなかった際に使用するデフォルト値 * @return {URLInit[]} */ function getURLFromLinkInit( links: LinkInit[], defaultOperator: AttributeSelectorOperator = "^=", ): URLInit[] { const resultURLs: URLInit[] = []; for (const link of links) { if (Array.isArray(link.url)) { for (const url of link.url) { if (typeof url === "string") { resultURLs.push({ url: url, selectorSyntax: defaultOperator }); } else { resultURLs.push(url); } } } else { const url = link.url; if (typeof url === "string") { resultURLs.push({ url: url, selectorSyntax: defaultOperator }); } else { resultURLs.push(url); } } } return resultURLs; }

deps.ts
依存関係をまとめてある
deps.ts
// CSSテキストを生成するのに使う export { toCSSText, } from "https://scrapbox.io/api/code/Mijinko/SCSSっぽい連想配列からCSSを生成するTypeScript関数/style.ts"; export type { ChildStyle, Style, } from "https://scrapbox.io/api/code/Mijinko/SCSSっぽい連想配列からCSSを生成するTypeScript関数/style.ts"; // クリップボードにコピーするやつ export { writeText } from "https://deno.land/x/copy_paste@v1.1.3/mod.ts";

生成済みのCSS
動作しないこともあるので注意
style.css
@import "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.1/css/all.min.css";.line span:not(.modal-image):not(.pointing-device-map) > a.link:not(.icon):not(:is([href$=".pdf"],[href^="https://github.com"],[href^="https://raw.githubusercontent.com"],[href^="https://gist.github.com"],[href^="https://twitter.com"],[href^="https://pbs.twimg.com"],[href^="https://video.twimg.com"],[href^="https://ja.wikipedia.org"],[href^="https://en.wikipedia.org"],[href^="https://www.amazon.co.jp"],[href^="https://amazon.co.jp"],[href^="https://discord.com"],[href^="https://discord.app"],[href^="https://discordapp.com"],[href^="https://discordstatus.com/"],[href^="https://www.cloudflare.com"],[href^="https://cdnjs.cloudflare.com"],[href^="https://store.steampowered.com"],[href^="https://help.steampowered.com"],[href^="https://steamcommunity.com"],[href^="https://s.team"],[href^="https://steamdb.info"],[href^="https://steamstat.us"],[href^="https://drive.google.com"],))::after{display:inline-block;padding-right:2px;font-size:.5em;font-weight:bold;font-family:'Font Awesome 6 Free', 'Font Awesome 6 Brands';content:'\f35d';cursor:text;}.line span:not(.deco-\.) > span > a.link:is([href$=".pdf"],[href^="https://github.com"],[href^="https://raw.githubusercontent.com"],[href^="https://gist.github.com"],[href^="https://twitter.com"],[href^="https://pbs.twimg.com"],[href^="https://video.twimg.com"],[href^="https://ja.wikipedia.org"],[href^="https://en.wikipedia.org"],[href^="https://www.amazon.co.jp"],[href^="https://amazon.co.jp"],[href^="https://discord.com"],[href^="https://discord.app"],[href^="https://discordapp.com"],[href^="https://discordstatus.com/"],[href^="https://www.cloudflare.com"],[href^="https://cdnjs.cloudflare.com"],[href^="https://store.steampowered.com"],[href^="https://help.steampowered.com"],[href^="https://steamcommunity.com"],[href^="https://s.team"],[href^="https://steamdb.info"],[href^="https://steamstat.us"],[href^="https://drive.google.com"],)::before{display:inline-block;width:auto;height:1em;line-height:1em;vertical-align:middle;text-align:center;background-size:contain;background-repeat:no-repeat;font-family:'Font Awesome 6 Free', 'Font Awesome 6 Brands';cursor:text;font-weight:400;margin-right:1px;}:is(.line, .line .deco) a.link:is([href$=".pdf"],)::before{content:'\f1c1';}:is(.line, .line .deco) a.link:is([href^="https://github.com"],[href^="https://raw.githubusercontent.com"],[href^="https://gist.github.com"],)::before{content:'\f09b';}:is(.line, .line .deco) a.link:is([href^="https://twitter.com"],[href^="https://pbs.twimg.com"],[href^="https://video.twimg.com"],)::before{content:'\f099';}:is(.line, .line .deco) a.link:is([href^="https://ja.wikipedia.org"],[href^="https://en.wikipedia.org"],)::before{content:'\f266';}:is(.line, .line .deco) a.link:is([href^="https://www.amazon.co.jp"],[href^="https://amazon.co.jp"],)::before{content:'\f270';}:is(.line, .line .deco) a.link:is([href^="https://discord.com"],[href^="https://discord.app"],[href^="https://discordapp.com"],[href^="https://discordstatus.com/"],)::before{content:'\f392';}:is(.line, .line .deco) a.link:is([href^="https://www.cloudflare.com"],[href^="https://cdnjs.cloudflare.com"],)::before{content:'\e07d';}:is(.line, .line .deco) a.link:is([href^="https://store.steampowered.com"],[href^="https://help.steampowered.com"],[href^="https://steamcommunity.com"],[href^="https://s.team"],)::before{content:'\f1b6';}:is(.line, .line .deco) a.link:is([href^="https://steamdb.info"],[href^="https://steamstat.us"],)::before{content:'\f3f6';}:is(.line, .line .deco) a.link:is([href^="https://drive.google.com"],)::before{content:'\f3aa';}i.fa-twitter::before{content:none;}.line .link .favicon.fa-twitter{margin:0;}