tangled
alpha
login
or
join now
treethought.xyz
/
obsidian-atmark
AT protocol bookmarking platforms in obsidian
8
fork
atom
overview
issues
pulls
pipelines
Compare changes
Choose any two refs to compare.
base:
view-docs
standard-site
responsive-sizing
pds-setting
margin-tags
margin
main
kipclip
client-cache
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
compare:
view-docs
standard-site
responsive-sizing
pds-setting
margin-tags
margin
main
kipclip
client-cache
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
go
+936
-293
17 changed files
expand all
collapse all
unified
split
bun.lock
package.json
src
commands
publishDocument.ts
lib
client.ts
clipper.ts
markdown
index.ts
leaflet.ts
pckt.ts
markdown.ts
standardsite
index.ts
leaflet.ts
pckt.ts
lib.ts
main.ts
settings.ts
views
standardfeed.ts
styles.css
+17
bun.lock
···
15
"@atcute/standard-site": "^1.0.0",
16
"obsidian": "latest",
17
"remark-parse": "^11.0.0",
0
18
"unified": "^11.0.5",
19
},
20
"devDependencies": {
···
540
541
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
542
0
0
543
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
544
545
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
546
547
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
0
0
0
0
548
549
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
550
···
667
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
668
669
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
0
0
670
671
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
672
···
774
775
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
776
0
0
777
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
0
0
0
0
778
779
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
780
···
801
"yaml-eslint-parser": ["yaml-eslint-parser@1.3.2", "", { "dependencies": { "eslint-visitor-keys": "^3.0.0", "yaml": "^2.0.0" } }, "sha512-odxVsHAkZYYglR30aPYRY4nUGJnoJ2y1ww2HDvZALo0BDETv9kWbi16J52eHs+PWRNmF4ub6nZqfVOeesOvntg=="],
802
803
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
0
0
804
805
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
806
···
15
"@atcute/standard-site": "^1.0.0",
16
"obsidian": "latest",
17
"remark-parse": "^11.0.0",
18
+
"remark-stringify": "^11.0.0",
19
"unified": "^11.0.5",
20
},
21
"devDependencies": {
···
541
542
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
543
544
+
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
545
+
546
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
547
548
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
549
550
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
551
+
552
+
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
553
+
554
+
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
555
556
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
557
···
674
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
675
676
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
677
+
678
+
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
679
680
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
681
···
783
784
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
785
786
+
"unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
787
+
788
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
789
+
790
+
"unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
791
+
792
+
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
793
794
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
795
···
816
"yaml-eslint-parser": ["yaml-eslint-parser@1.3.2", "", { "dependencies": { "eslint-visitor-keys": "^3.0.0", "yaml": "^2.0.0" } }, "sha512-odxVsHAkZYYglR30aPYRY4nUGJnoJ2y1ww2HDvZALo0BDETv9kWbi16J52eHs+PWRNmF4ub6nZqfVOeesOvntg=="],
817
818
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
819
+
820
+
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
821
822
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
823
+1
package.json
···
35
"@atcute/standard-site": "^1.0.0",
36
"obsidian": "latest",
37
"remark-parse": "^11.0.0",
0
38
"unified": "^11.0.5"
39
}
40
}
···
35
"@atcute/standard-site": "^1.0.0",
36
"obsidian": "latest",
37
"remark-parse": "^11.0.0",
38
+
"remark-stringify": "^11.0.0",
39
"unified": "^11.0.5"
40
}
41
}
+2
-17
src/commands/publishDocument.ts
···
1
import { Notice, TFile } from "obsidian";
2
import type ATmarkPlugin from "../main";
3
-
import { createDocument, putDocument, getPublication, markdownToLeafletContent, stripMarkdown, markdownToPcktContent } from "../lib";
4
import { PublicationSelection, SelectPublicationModal } from "../components/selectPublicationModal";
5
-
import { parseResourceUri, type ResourceUri, } from "@atcute/lexicons";
6
import { SiteStandardDocument, SiteStandardPublication } from "@atcute/standard-site";
7
import { PubLeafletContent } from "@atcute/leaflet";
8
import { BlogPcktContent } from "@atcute/pckt";
···
41
new Notice(`Error publishing document: ${message}`);
42
console.error("Publish document error:", error);
43
}
44
-
}
45
-
46
-
function buildDocumentUrl(pubUrl: string, docUri: string, record: SiteStandardDocument.Main): string {
47
-
const baseUrl = pubUrl.replace(/\/$/, '');
48
-
49
-
// leaflet does not use path, url just uses rkey
50
-
if (record.path === undefined || record.path === '') {
51
-
const parsed = parseResourceUri(docUri)
52
-
if (parsed.ok) {
53
-
return `${baseUrl}/${parsed.value.rkey}`;
54
-
}
55
-
return ""
56
-
}
57
-
58
-
return `${baseUrl}/${record.path}`
59
}
60
61
async function updateFrontMatter(
···
1
import { Notice, TFile } from "obsidian";
2
import type ATmarkPlugin from "../main";
3
+
import { createDocument, putDocument, getPublication, markdownToLeafletContent, stripMarkdown, markdownToPcktContent, buildDocumentUrl } from "../lib";
4
import { PublicationSelection, SelectPublicationModal } from "../components/selectPublicationModal";
5
+
import { type ResourceUri, } from "@atcute/lexicons";
6
import { SiteStandardDocument, SiteStandardPublication } from "@atcute/standard-site";
7
import { PubLeafletContent } from "@atcute/leaflet";
8
import { BlogPcktContent } from "@atcute/pckt";
···
41
new Notice(`Error publishing document: ${message}`);
42
console.error("Publish document error:", error);
43
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
44
}
45
46
async function updateFrontMatter(
+3
src/lib/client.ts
···
26
return this.hh.cm.session;
27
}
28
0
0
0
29
}
30
31
export class Handler implements FetchHandlerObject {
···
26
return this.hh.cm.session;
27
}
28
29
+
getActor(identifier: string): Promise<ResolvedActor> {
30
+
return this.hh.getActor(identifier);
31
+
}
32
}
33
34
export class Handler implements FetchHandlerObject {
+118
src/lib/clipper.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { ATRecord, buildDocumentUrl } from "lib";
2
+
import { Main as Document } from "@atcute/standard-site/types/document";
3
+
import { Main as Publication } from "@atcute/standard-site/types/publication";
4
+
import { is, parseResourceUri } from "@atcute/lexicons";
5
+
import { Notice, TFile } from "obsidian";
6
+
import ATmarkPlugin from "main";
7
+
import { leafletContentToMarkdown } from "./markdown/leaflet";
8
+
import { pcktContentToMarkdown } from "./markdown/pckt";
9
+
import { ResolvedActor } from "@atcute/identity-resolver";
10
+
import { PubLeafletContent } from "@atcute/leaflet";
11
+
import { BlogPcktContent } from "@atcute/pckt";
12
+
13
+
14
+
function bskyLink(handle: string) {
15
+
return `https://bsky.app/profile/${handle}`;
16
+
}
17
+
18
+
export class Clipper {
19
+
plugin: ATmarkPlugin;
20
+
21
+
constructor(plugin: ATmarkPlugin) {
22
+
this.plugin = plugin;
23
+
}
24
+
25
+
safeFilePath(title: string, clipDir: string) {
26
+
const safeTitle = title.replace(/[/\\?%*:|"<>]/g, "-").substring(0, 50);
27
+
return `${clipDir}/${safeTitle}.md`;
28
+
}
29
+
30
+
existsInClipDir(doc: ATRecord<Document>) {
31
+
const vault = this.plugin.app.vault;
32
+
const clipDir = this.plugin.settings.clipDir
33
+
34
+
35
+
const filePath = this.safeFilePath(doc.value.title, clipDir);
36
+
const file = vault.getAbstractFileByPath(filePath);
37
+
return file !== null;
38
+
}
39
+
40
+
41
+
async writeFrontmatter(file: TFile, doc: ATRecord<Document>, pub: ATRecord<Publication>) {
42
+
let actor: ResolvedActor | null = null;
43
+
const repoParsed = parseResourceUri(doc.uri);
44
+
if (repoParsed.ok) {
45
+
actor = await this.plugin.client.getActor(repoParsed.value.repo);
46
+
}
47
+
// Add frontmatter using Obsidian's processFrontMatter
48
+
await this.plugin.app.fileManager.processFrontMatter(file, (fm: Record<string, unknown>) => {
49
+
fm["title"] = doc.value.title;
50
+
if (actor && actor.handle) {
51
+
fm["author"] = `[${actor.handle}](${bskyLink(actor.handle)})`;
52
+
}
53
+
fm["aturi"] = doc.uri;
54
+
55
+
let docUrl = "";
56
+
57
+
// pubUrl is at:// record uri or https:// for loose document
58
+
// fetch pub if at:// so we can get the url
59
+
// otherwise just use the url as is
60
+
if (doc.value.site.startsWith("https://")) {
61
+
docUrl = buildDocumentUrl(doc.value.site, doc.uri, doc.value);
62
+
} else {
63
+
docUrl = buildDocumentUrl(pub.value.url, doc.uri, doc.value);
64
+
65
+
}
66
+
if (docUrl) {
67
+
fm["url"] = docUrl;
68
+
}
69
+
});
70
+
}
71
+
72
+
async clipDocument(doc: ATRecord<Document>, pub: ATRecord<Publication>) {
73
+
const vault = this.plugin.app.vault;
74
+
const clipDir = this.plugin.settings.clipDir
75
+
76
+
const parsed = parseResourceUri(pub.uri);
77
+
if (!parsed.ok) {
78
+
throw new Error(`Invalid publication URI: ${pub.uri}`);
79
+
}
80
+
if (!vault.getAbstractFileByPath(clipDir)) {
81
+
await vault.createFolder(clipDir);
82
+
}
83
+
const filePath = this.safeFilePath(doc.value.title, clipDir);
84
+
85
+
let content = `# ${doc.value.title}\n\n`;
86
+
87
+
if (doc.value.description) {
88
+
content += `> ${doc.value.description}\n\n`;
89
+
}
90
+
91
+
content += `---\n\n`;
92
+
93
+
let bodyContent = "";
94
+
if (doc.value.content) {
95
+
if (is(PubLeafletContent.mainSchema, doc.value.content)) {
96
+
bodyContent = leafletContentToMarkdown(doc.value.content);
97
+
} else if (is(BlogPcktContent.mainSchema, doc.value.content)) {
98
+
bodyContent = pcktContentToMarkdown(doc.value.content);
99
+
}
100
+
}
101
+
102
+
if (!bodyContent && doc.value.textContent) {
103
+
bodyContent = doc.value.textContent;
104
+
}
105
+
106
+
content += bodyContent;
107
+
108
+
const file = await vault.create(filePath, content);
109
+
await this.writeFrontmatter(file, doc, pub);
110
+
111
+
112
+
const leaf = this.plugin.app.workspace.getLeaf(false);
113
+
await leaf.openFile(file);
114
+
115
+
new Notice(`Clipped document to ${filePath}`);
116
+
}
117
+
}
118
+
+46
src/lib/markdown/index.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import { unified } from "unified";
2
+
import remarkParse from "remark-parse";
3
+
import type { Root, RootContent } from "mdast";
4
+
5
+
export function parseMarkdown(markdown: string): Root {
6
+
return unified().use(remarkParse).parse(markdown);
7
+
}
8
+
9
+
export function extractText(node: RootContent | Root): string {
10
+
if (node.type === "text") {
11
+
return node.value;
12
+
}
13
+
14
+
if (node.type === "inlineCode") {
15
+
return node.value;
16
+
}
17
+
18
+
if ("children" in node && Array.isArray(node.children)) {
19
+
return node.children.map(extractText).join("");
20
+
}
21
+
22
+
if ("value" in node && typeof node.value === "string") {
23
+
return node.value;
24
+
}
25
+
26
+
return "";
27
+
}
28
+
29
+
/**
30
+
* Strip markdown formatting to plain text
31
+
* Used for the textContent field in standard.site documents
32
+
*/
33
+
export function stripMarkdown(markdown: string): string {
34
+
const tree = parseMarkdown(markdown);
35
+
return tree.children.map(extractText).join("\n\n").trim();
36
+
}
37
+
38
+
export function cleanPlaintext(text: string): string {
39
+
return text.trim();
40
+
}
41
+
42
+
export type { Root, RootContent };
43
+
44
+
export { markdownToPcktContent, pcktContentToMarkdown } from "./pckt";
45
+
export { markdownToLeafletContent, leafletContentToMarkdown } from "./leaflet";
46
+
+191
src/lib/markdown/leaflet.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import type { RootContent, Root } from "mdast";
2
+
import { unified } from "unified";
3
+
import remarkStringify from "remark-stringify";
4
+
import {
5
+
PubLeafletBlocksUnorderedList,
6
+
PubLeafletContent,
7
+
PubLeafletPagesLinearDocument,
8
+
} from "@atcute/leaflet";
9
+
import { parseMarkdown, extractText, cleanPlaintext } from "../markdown";
10
+
11
+
export function markdownToLeafletContent(markdown: string): PubLeafletContent.Main {
12
+
const tree = parseMarkdown(markdown);
13
+
const blocks: PubLeafletPagesLinearDocument.Block[] = [];
14
+
15
+
for (const node of tree.children) {
16
+
const block = convertNodeToBlock(node);
17
+
if (block) {
18
+
blocks.push(block);
19
+
}
20
+
}
21
+
22
+
return {
23
+
$type: "pub.leaflet.content",
24
+
pages: [{
25
+
$type: "pub.leaflet.pages.linearDocument",
26
+
blocks,
27
+
}],
28
+
};
29
+
}
30
+
31
+
function convertNodeToBlock(node: RootContent): PubLeafletPagesLinearDocument.Block | null {
32
+
switch (node.type) {
33
+
case "heading":
34
+
return {
35
+
block: {
36
+
$type: "pub.leaflet.blocks.header",
37
+
level: node.depth,
38
+
plaintext: extractText(node),
39
+
},
40
+
alignment: "pub.leaflet.pages.linearDocument#textAlignLeft",
41
+
};
42
+
43
+
case "paragraph":
44
+
return {
45
+
block: {
46
+
$type: "pub.leaflet.blocks.text",
47
+
plaintext: extractText(node),
48
+
textSize: "default",
49
+
},
50
+
alignment: "pub.leaflet.pages.linearDocument#textAlignLeft",
51
+
};
52
+
53
+
case "list": {
54
+
const listItems: PubLeafletBlocksUnorderedList.ListItem[] = node.children.map((item) => ({
55
+
$type: "pub.leaflet.blocks.unorderedList#listItem",
56
+
content: {
57
+
$type: "pub.leaflet.blocks.text",
58
+
plaintext: extractText(item),
59
+
textSize: "default",
60
+
},
61
+
}));
62
+
63
+
return {
64
+
block: {
65
+
$type: "pub.leaflet.blocks.unorderedList",
66
+
children: listItems,
67
+
},
68
+
alignment: "pub.leaflet.pages.linearDocument#textAlignLeft",
69
+
};
70
+
}
71
+
72
+
case "code":
73
+
return {
74
+
block: {
75
+
$type: "pub.leaflet.blocks.code",
76
+
plaintext: node.value,
77
+
language: node.lang || undefined,
78
+
},
79
+
alignment: "pub.leaflet.pages.linearDocument#textAlignLeft",
80
+
};
81
+
82
+
case "thematicBreak":
83
+
return {
84
+
block: {
85
+
$type: "pub.leaflet.blocks.horizontalRule",
86
+
},
87
+
alignment: "pub.leaflet.pages.linearDocument#textAlignLeft",
88
+
};
89
+
90
+
case "blockquote":
91
+
return {
92
+
block: {
93
+
$type: "pub.leaflet.blocks.blockquote",
94
+
plaintext: extractText(node),
95
+
},
96
+
alignment: "pub.leaflet.pages.linearDocument#textAlignLeft",
97
+
};
98
+
99
+
default:
100
+
return null;
101
+
}
102
+
}
103
+
104
+
export function leafletContentToMarkdown(content: PubLeafletContent.Main): string {
105
+
const mdastNodes: RootContent[] = [];
106
+
107
+
for (const page of content.pages) {
108
+
if (page.$type !== "pub.leaflet.pages.linearDocument") {
109
+
continue;
110
+
}
111
+
112
+
for (const item of page.blocks) {
113
+
const block = item.block;
114
+
const node = leafletBlockToMdast(block);
115
+
if (node) {
116
+
mdastNodes.push(node);
117
+
}
118
+
}
119
+
}
120
+
121
+
const root: Root = {
122
+
type: "root",
123
+
children: mdastNodes,
124
+
};
125
+
126
+
return unified().use(remarkStringify).stringify(root);
127
+
}
128
+
129
+
// Extract the union type of all possible leaflet blocks from the Block interface
130
+
type LeafletBlockType = PubLeafletPagesLinearDocument.Block['block'];
131
+
132
+
function leafletBlockToMdast(block: LeafletBlockType): RootContent | null {
133
+
switch (block.$type) {
134
+
case "pub.leaflet.blocks.header":
135
+
return {
136
+
type: "heading",
137
+
depth: block.level as 1 | 2 | 3 | 4 | 5 | 6,
138
+
children: [{ type: "text", value: cleanPlaintext(block.plaintext) }],
139
+
};
140
+
141
+
case "pub.leaflet.blocks.text":
142
+
return {
143
+
type: "paragraph",
144
+
children: [{ type: "text", value: cleanPlaintext(block.plaintext) }],
145
+
};
146
+
147
+
case "pub.leaflet.blocks.unorderedList":
148
+
return {
149
+
type: "list",
150
+
ordered: false,
151
+
spread: false,
152
+
children: block.children.map((item: PubLeafletBlocksUnorderedList.ListItem) => {
153
+
// Extract plaintext from the content, which can be Header, Image, or Text
154
+
const plaintext = 'plaintext' in item.content ? cleanPlaintext(item.content.plaintext) : '';
155
+
return {
156
+
type: "listItem",
157
+
spread: false,
158
+
children: [{
159
+
type: "paragraph",
160
+
children: [{ type: "text", value: plaintext }],
161
+
}],
162
+
};
163
+
}),
164
+
};
165
+
166
+
case "pub.leaflet.blocks.code":
167
+
return {
168
+
type: "code",
169
+
lang: block.language || null,
170
+
meta: null,
171
+
value: block.plaintext, // Keep code blocks as-is to preserve formatting
172
+
};
173
+
174
+
case "pub.leaflet.blocks.horizontalRule":
175
+
return {
176
+
type: "thematicBreak",
177
+
};
178
+
179
+
case "pub.leaflet.blocks.blockquote":
180
+
return {
181
+
type: "blockquote",
182
+
children: [{
183
+
type: "paragraph",
184
+
children: [{ type: "text", value: cleanPlaintext(block.plaintext) }],
185
+
}],
186
+
};
187
+
188
+
default:
189
+
return null;
190
+
}
191
+
}
+223
src/lib/markdown/pckt.ts
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
import type { RootContent, Root } from "mdast";
2
+
import { unified } from "unified";
3
+
import remarkStringify from "remark-stringify";
4
+
import {
5
+
BlogPcktBlockListItem,
6
+
BlogPcktBlockText,
7
+
BlogPcktBlockHeading,
8
+
BlogPcktBlockCodeBlock,
9
+
BlogPcktBlockBulletList,
10
+
BlogPcktBlockOrderedList,
11
+
BlogPcktBlockHorizontalRule,
12
+
BlogPcktBlockBlockquote,
13
+
BlogPcktContent,
14
+
} from "@atcute/pckt";
15
+
import { parseMarkdown, extractText, cleanPlaintext } from "../markdown";
16
+
17
+
type PcktBlock =
18
+
| BlogPcktBlockText.Main
19
+
| BlogPcktBlockHeading.Main
20
+
| BlogPcktBlockCodeBlock.Main
21
+
| BlogPcktBlockBulletList.Main
22
+
| BlogPcktBlockOrderedList.Main
23
+
| BlogPcktBlockHorizontalRule.Main
24
+
| BlogPcktBlockBlockquote.Main;
25
+
26
+
export function markdownToPcktContent(markdown: string): BlogPcktContent.Main {
27
+
const tree = parseMarkdown(markdown);
28
+
const items: PcktBlock[] = [];
29
+
30
+
for (const node of tree.children) {
31
+
const block = convertNodeToBlock(node);
32
+
if (block) {
33
+
items.push(block);
34
+
}
35
+
}
36
+
37
+
return {
38
+
$type: "blog.pckt.content",
39
+
items,
40
+
} as BlogPcktContent.Main;
41
+
}
42
+
43
+
function convertNodeToBlock(node: RootContent): PcktBlock | null {
44
+
switch (node.type) {
45
+
case "heading": {
46
+
const block: BlogPcktBlockHeading.Main = {
47
+
$type: "blog.pckt.block.heading",
48
+
level: node.depth,
49
+
plaintext: extractText(node),
50
+
};
51
+
return block;
52
+
}
53
+
54
+
case "paragraph": {
55
+
const block: BlogPcktBlockText.Main = {
56
+
$type: "blog.pckt.block.text",
57
+
plaintext: extractText(node),
58
+
};
59
+
return block;
60
+
}
61
+
62
+
case "list": {
63
+
const listItems: BlogPcktBlockListItem.Main[] = node.children.map((item) => ({
64
+
$type: "blog.pckt.block.listItem",
65
+
content: [{
66
+
$type: "blog.pckt.block.text",
67
+
plaintext: extractText(item),
68
+
}],
69
+
}));
70
+
71
+
if (node.ordered) {
72
+
const block: BlogPcktBlockOrderedList.Main = {
73
+
$type: "blog.pckt.block.orderedList",
74
+
content: listItems,
75
+
};
76
+
return block;
77
+
} else {
78
+
const block: BlogPcktBlockBulletList.Main = {
79
+
$type: "blog.pckt.block.bulletList",
80
+
content: listItems,
81
+
};
82
+
return block;
83
+
}
84
+
}
85
+
86
+
case "code": {
87
+
const block: BlogPcktBlockCodeBlock.Main = {
88
+
$type: "blog.pckt.block.codeBlock",
89
+
plaintext: node.value,
90
+
language: node.lang || undefined,
91
+
};
92
+
return block;
93
+
}
94
+
95
+
case "thematicBreak": {
96
+
const block: BlogPcktBlockHorizontalRule.Main = {
97
+
$type: "blog.pckt.block.horizontalRule",
98
+
};
99
+
return block;
100
+
}
101
+
102
+
case "blockquote": {
103
+
const block: BlogPcktBlockBlockquote.Main = {
104
+
$type: "blog.pckt.block.blockquote",
105
+
content: [{
106
+
$type: "blog.pckt.block.text",
107
+
plaintext: extractText(node),
108
+
}],
109
+
};
110
+
return block;
111
+
}
112
+
113
+
default:
114
+
return null;
115
+
}
116
+
}
117
+
118
+
/**
119
+
* Convert pckt content to markdown string
120
+
*/
121
+
export function pcktContentToMarkdown(content: BlogPcktContent.Main): string {
122
+
const mdastNodes: RootContent[] = [];
123
+
124
+
for (const block of content.items) {
125
+
const node = pcktBlockToMdast(block);
126
+
if (node) {
127
+
mdastNodes.push(node);
128
+
}
129
+
}
130
+
131
+
const root: Root = {
132
+
type: "root",
133
+
children: mdastNodes,
134
+
};
135
+
136
+
return unified().use(remarkStringify).stringify(root);
137
+
}
138
+
139
+
function pcktBlockToMdast(block: PcktBlock): RootContent | null {
140
+
switch (block.$type) {
141
+
case "blog.pckt.block.heading":
142
+
return {
143
+
type: "heading",
144
+
depth: block.level as 1 | 2 | 3 | 4 | 5 | 6,
145
+
children: [{ type: "text", value: cleanPlaintext(block.plaintext) }],
146
+
};
147
+
148
+
case "blog.pckt.block.text":
149
+
return {
150
+
type: "paragraph",
151
+
children: [{ type: "text", value: cleanPlaintext(block.plaintext) }],
152
+
};
153
+
154
+
case "blog.pckt.block.bulletList":
155
+
return {
156
+
type: "list",
157
+
ordered: false,
158
+
spread: false,
159
+
children: block.content.map((item: BlogPcktBlockListItem.Main) => {
160
+
const text = item.content
161
+
.map((c) => ('plaintext' in c ? cleanPlaintext(c.plaintext) : ''))
162
+
.join(" ");
163
+
return {
164
+
type: "listItem",
165
+
spread: false,
166
+
children: [{
167
+
type: "paragraph",
168
+
children: [{ type: "text", value: text }],
169
+
}],
170
+
};
171
+
}),
172
+
};
173
+
174
+
case "blog.pckt.block.orderedList":
175
+
return {
176
+
type: "list",
177
+
ordered: true,
178
+
spread: false,
179
+
children: block.content.map((item: BlogPcktBlockListItem.Main) => {
180
+
const text = item.content
181
+
.map((c) => ('plaintext' in c ? cleanPlaintext(c.plaintext) : ''))
182
+
.join(" ");
183
+
return {
184
+
type: "listItem",
185
+
spread: false,
186
+
children: [{
187
+
type: "paragraph",
188
+
children: [{ type: "text", value: text }],
189
+
}],
190
+
};
191
+
}),
192
+
};
193
+
194
+
case "blog.pckt.block.codeBlock":
195
+
return {
196
+
type: "code",
197
+
lang: block.language || null,
198
+
meta: null,
199
+
value: block.plaintext,
200
+
};
201
+
202
+
case "blog.pckt.block.horizontalRule":
203
+
return {
204
+
type: "thematicBreak",
205
+
};
206
+
207
+
case "blog.pckt.block.blockquote": {
208
+
const text = block.content
209
+
.map((c: BlogPcktBlockText.Main) => cleanPlaintext(c.plaintext))
210
+
.join("\n");
211
+
return {
212
+
type: "blockquote",
213
+
children: [{
214
+
type: "paragraph",
215
+
children: [{ type: "text", value: text }],
216
+
}],
217
+
};
218
+
}
219
+
220
+
default:
221
+
return null;
222
+
}
223
+
}
-38
src/lib/markdown.ts
···
1
-
import { unified } from "unified";
2
-
import remarkParse from "remark-parse";
3
-
import type { Root, RootContent } from "mdast";
4
-
5
-
export function parseMarkdown(markdown: string): Root {
6
-
return unified().use(remarkParse).parse(markdown);
7
-
}
8
-
9
-
export function extractText(node: RootContent | Root): string {
10
-
if (node.type === "text") {
11
-
return node.value;
12
-
}
13
-
14
-
if (node.type === "inlineCode") {
15
-
return node.value;
16
-
}
17
-
18
-
if ("children" in node && Array.isArray(node.children)) {
19
-
return node.children.map(extractText).join("");
20
-
}
21
-
22
-
if ("value" in node && typeof node.value === "string") {
23
-
return node.value;
24
-
}
25
-
26
-
return "";
27
-
}
28
-
29
-
/**
30
-
* Strip markdown formatting to plain text
31
-
* Used for the textContent field in standard.site documents
32
-
*/
33
-
export function stripMarkdown(markdown: string): string {
34
-
const tree = parseMarkdown(markdown);
35
-
return tree.children.map(extractText).join("\n\n").trim();
36
-
}
37
-
38
-
export type { Root, RootContent };
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
+16
-1
src/lib/standardsite/index.ts
···
9
import { ATRecord } from "lib";
10
import { SiteStandardDocument, SiteStandardGraphSubscription, SiteStandardPublication } from "@atcute/standard-site";
11
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
12
export async function getPublicationDocuments(client: Client, repo: string, pubUri: ResourceUri) {
13
const response = await ok(client.call(ComAtprotoRepoListRecords, {
14
params: {
···
18
},
19
}));
20
21
-
// filter records by publication uri
22
const pubDocs = response.records.filter(record => {
23
const parsed = parse(SiteStandardDocument.mainSchema, record.value);
24
return parsed.site === pubUri;
···
9
import { ATRecord } from "lib";
10
import { SiteStandardDocument, SiteStandardGraphSubscription, SiteStandardPublication } from "@atcute/standard-site";
11
12
+
export function buildDocumentUrl(pubUrl: string, docUri: string, record: SiteStandardDocument.Main): string {
13
+
const baseUrl = pubUrl.replace(/\/$/, '');
14
+
15
+
// leaflet does not use path, url just uses rkey
16
+
if (record.path === undefined || record.path === '') {
17
+
const parsed = parseResourceUri(docUri)
18
+
if (parsed.ok) {
19
+
return `${baseUrl}/${parsed.value.rkey}`;
20
+
}
21
+
return ""
22
+
}
23
+
24
+
return `${baseUrl}/${record.path}`
25
+
}
26
+
27
+
28
export async function getPublicationDocuments(client: Client, repo: string, pubUri: ResourceUri) {
29
const response = await ok(client.call(ComAtprotoRepoListRecords, {
30
params: {
···
34
},
35
}));
36
0
37
const pubDocs = response.records.filter(record => {
38
const parsed = parse(SiteStandardDocument.mainSchema, record.value);
39
return parsed.site === pubUri;
-100
src/lib/standardsite/leaflet.ts
···
1
-
import type { RootContent } from "mdast";
2
-
import {
3
-
PubLeafletBlocksUnorderedList,
4
-
PubLeafletContent,
5
-
PubLeafletPagesLinearDocument,
6
-
} from "@atcute/leaflet";
7
-
import { parseMarkdown, extractText } from "../markdown";
8
-
9
-
export function markdownToLeafletContent(markdown: string): PubLeafletContent.Main {
10
-
const tree = parseMarkdown(markdown);
11
-
const blocks: PubLeafletPagesLinearDocument.Block[] = [];
12
-
13
-
for (const node of tree.children) {
14
-
const block = convertNodeToBlock(node);
15
-
if (block) {
16
-
blocks.push(block);
17
-
}
18
-
}
19
-
20
-
return {
21
-
$type: "pub.leaflet.content",
22
-
pages: [{
23
-
$type: "pub.leaflet.pages.linearDocument",
24
-
blocks,
25
-
}],
26
-
};
27
-
}
28
-
29
-
function convertNodeToBlock(node: RootContent): PubLeafletPagesLinearDocument.Block | null {
30
-
switch (node.type) {
31
-
case "heading":
32
-
return {
33
-
block: {
34
-
$type: "pub.leaflet.blocks.header",
35
-
level: node.depth,
36
-
plaintext: extractText(node),
37
-
},
38
-
alignment: "pub.leaflet.pages.linearDocument#textAlignLeft",
39
-
};
40
-
41
-
case "paragraph":
42
-
return {
43
-
block: {
44
-
$type: "pub.leaflet.blocks.text",
45
-
plaintext: extractText(node),
46
-
textSize: "default",
47
-
},
48
-
alignment: "pub.leaflet.pages.linearDocument#textAlignLeft",
49
-
};
50
-
51
-
case "list": {
52
-
const listItems: PubLeafletBlocksUnorderedList.ListItem[] = node.children.map((item) => ({
53
-
$type: "pub.leaflet.blocks.unorderedList#listItem",
54
-
content: {
55
-
$type: "pub.leaflet.blocks.text",
56
-
plaintext: extractText(item),
57
-
textSize: "default",
58
-
},
59
-
}));
60
-
61
-
return {
62
-
block: {
63
-
$type: "pub.leaflet.blocks.unorderedList",
64
-
children: listItems,
65
-
},
66
-
alignment: "pub.leaflet.pages.linearDocument#textAlignLeft",
67
-
};
68
-
}
69
-
70
-
case "code":
71
-
return {
72
-
block: {
73
-
$type: "pub.leaflet.blocks.code",
74
-
plaintext: node.value,
75
-
language: node.lang || undefined,
76
-
},
77
-
alignment: "pub.leaflet.pages.linearDocument#textAlignLeft",
78
-
};
79
-
80
-
case "thematicBreak":
81
-
return {
82
-
block: {
83
-
$type: "pub.leaflet.blocks.horizontalRule",
84
-
},
85
-
alignment: "pub.leaflet.pages.linearDocument#textAlignLeft",
86
-
};
87
-
88
-
case "blockquote":
89
-
return {
90
-
block: {
91
-
$type: "pub.leaflet.blocks.blockquote",
92
-
plaintext: extractText(node),
93
-
},
94
-
alignment: "pub.leaflet.pages.linearDocument#textAlignLeft",
95
-
};
96
-
97
-
default:
98
-
return null;
99
-
}
100
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-119
src/lib/standardsite/pckt.ts
···
1
-
/**
2
-
* Markdown to Pckt blocks parser
3
-
* Converts markdown content to blog.pckt.content format
4
-
*/
5
-
6
-
import type { RootContent } from "mdast";
7
-
import {
8
-
BlogPcktBlockListItem,
9
-
BlogPcktBlockText,
10
-
BlogPcktBlockHeading,
11
-
BlogPcktBlockCodeBlock,
12
-
BlogPcktBlockBulletList,
13
-
BlogPcktBlockOrderedList,
14
-
BlogPcktBlockHorizontalRule,
15
-
BlogPcktBlockBlockquote,
16
-
BlogPcktContent,
17
-
} from "@atcute/pckt";
18
-
import { parseMarkdown, extractText } from "../markdown";
19
-
20
-
type PcktBlock =
21
-
| BlogPcktBlockText.Main
22
-
| BlogPcktBlockHeading.Main
23
-
| BlogPcktBlockCodeBlock.Main
24
-
| BlogPcktBlockBulletList.Main
25
-
| BlogPcktBlockOrderedList.Main
26
-
| BlogPcktBlockHorizontalRule.Main
27
-
| BlogPcktBlockBlockquote.Main;
28
-
29
-
export function markdownToPcktContent(markdown: string): BlogPcktContent.Main {
30
-
const tree = parseMarkdown(markdown);
31
-
const items: PcktBlock[] = [];
32
-
33
-
for (const node of tree.children) {
34
-
const block = convertNodeToBlock(node);
35
-
if (block) {
36
-
items.push(block);
37
-
}
38
-
}
39
-
40
-
return {
41
-
$type: "blog.pckt.content",
42
-
items,
43
-
} as BlogPcktContent.Main;
44
-
}
45
-
46
-
function convertNodeToBlock(node: RootContent): PcktBlock | null {
47
-
switch (node.type) {
48
-
case "heading": {
49
-
const block: BlogPcktBlockHeading.Main = {
50
-
$type: "blog.pckt.block.heading",
51
-
level: node.depth,
52
-
plaintext: extractText(node),
53
-
};
54
-
return block;
55
-
}
56
-
57
-
case "paragraph": {
58
-
const block: BlogPcktBlockText.Main = {
59
-
$type: "blog.pckt.block.text",
60
-
plaintext: extractText(node),
61
-
};
62
-
return block;
63
-
}
64
-
65
-
case "list": {
66
-
const listItems: BlogPcktBlockListItem.Main[] = node.children.map((item) => ({
67
-
$type: "blog.pckt.block.listItem",
68
-
content: [{
69
-
$type: "blog.pckt.block.text",
70
-
plaintext: extractText(item),
71
-
}],
72
-
}));
73
-
74
-
if (node.ordered) {
75
-
const block: BlogPcktBlockOrderedList.Main = {
76
-
$type: "blog.pckt.block.orderedList",
77
-
content: listItems,
78
-
};
79
-
return block;
80
-
} else {
81
-
const block: BlogPcktBlockBulletList.Main = {
82
-
$type: "blog.pckt.block.bulletList",
83
-
content: listItems,
84
-
};
85
-
return block;
86
-
}
87
-
}
88
-
89
-
case "code": {
90
-
const block: BlogPcktBlockCodeBlock.Main = {
91
-
$type: "blog.pckt.block.codeBlock",
92
-
plaintext: node.value,
93
-
language: node.lang || undefined,
94
-
};
95
-
return block;
96
-
}
97
-
98
-
case "thematicBreak": {
99
-
const block: BlogPcktBlockHorizontalRule.Main = {
100
-
$type: "blog.pckt.block.horizontalRule",
101
-
};
102
-
return block;
103
-
}
104
-
105
-
case "blockquote": {
106
-
const block: BlogPcktBlockBlockquote.Main = {
107
-
$type: "blog.pckt.block.blockquote",
108
-
content: [{
109
-
$type: "blog.pckt.block.text",
110
-
plaintext: extractText(node),
111
-
}],
112
-
};
113
-
return block;
114
-
}
115
-
116
-
default:
117
-
return null;
118
-
}
119
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
+7
-3
src/lib.ts
···
24
} from "./lib/bookmarks/margin";
25
26
export {
0
27
createDocument,
28
putDocument,
29
getPublication,
30
getPublications,
31
getSubscribedPublications,
32
createPublication,
0
33
} from "./lib/standardsite";
34
35
-
export { markdownToLeafletContent } from "./lib/standardsite/leaflet";
36
-
export { markdownToPcktContent } from "./lib/standardsite/pckt";
37
-
export { stripMarkdown } from "./lib/markdown";
0
0
38
39
export type ATRecord<T> = Record & { value: T };
···
24
} from "./lib/bookmarks/margin";
25
26
export {
27
+
getPublicationDocuments,
28
createDocument,
29
putDocument,
30
getPublication,
31
getPublications,
32
getSubscribedPublications,
33
createPublication,
34
+
buildDocumentUrl
35
} from "./lib/standardsite";
36
37
+
export {
38
+
stripMarkdown,
39
+
markdownToLeafletContent,
40
+
markdownToPcktContent,
41
+
} from "./lib/markdown";
42
43
export type ATRecord<T> = Record & { value: T };
+4
-1
src/main.ts
···
4
import { publishFileAsDocument } from "./commands/publishDocument";
5
import { StandardFeedView, VIEW_STANDARD_FEED } from "views/standardfeed";
6
import { ATClient } from "lib/client";
0
7
8
export default class ATmarkPlugin extends Plugin {
9
settings: AtProtoSettings = DEFAULT_SETTINGS;
10
-
client: ATClient
0
11
12
async onload() {
13
await this.loadSettings();
···
17
password: this.settings.appPassword,
18
};
19
this.client = new ATClient(creds);
0
20
21
this.registerView(VIEW_TYPE_ATMARK, (leaf) => {
22
return new ATmarkView(leaf, this);
···
4
import { publishFileAsDocument } from "./commands/publishDocument";
5
import { StandardFeedView, VIEW_STANDARD_FEED } from "views/standardfeed";
6
import { ATClient } from "lib/client";
7
+
import { Clipper } from "lib/clipper";
8
9
export default class ATmarkPlugin extends Plugin {
10
settings: AtProtoSettings = DEFAULT_SETTINGS;
11
+
client: ATClient;
12
+
clipper: Clipper;
13
14
async onload() {
15
await this.loadSettings();
···
19
password: this.settings.appPassword,
20
};
21
this.client = new ATClient(creds);
22
+
this.clipper = new Clipper(this);
23
24
this.registerView(VIEW_TYPE_ATMARK, (leaf) => {
25
return new ATmarkView(leaf, this);
+13
src/settings.ts
···
4
export interface AtProtoSettings {
5
identifier: string;
6
appPassword: string;
0
7
}
8
9
export const DEFAULT_SETTINGS: AtProtoSettings = {
10
identifier: "",
11
appPassword: "",
0
12
};
13
14
export class SettingTab extends PluginSettingTab {
···
48
await this.plugin.saveSettings();
49
});
50
});
0
0
0
0
0
0
0
0
0
0
0
51
}
52
}
···
4
export interface AtProtoSettings {
5
identifier: string;
6
appPassword: string;
7
+
clipDir: string;
8
}
9
10
export const DEFAULT_SETTINGS: AtProtoSettings = {
11
identifier: "",
12
appPassword: "",
13
+
clipDir: "AtmosphereClips",
14
};
15
16
export class SettingTab extends PluginSettingTab {
···
50
await this.plugin.saveSettings();
51
});
52
});
53
+
new Setting(containerEl)
54
+
.setName("Clip directory")
55
+
.setDesc("Directory in your vault to save clips (will be created if it doesn't exist)")
56
+
.addText((text) =>
57
+
text
58
+
.setValue(this.plugin.settings.clipDir)
59
+
.onChange(async (value) => {
60
+
this.plugin.settings.clipDir = value;
61
+
await this.plugin.saveSettings();
62
+
})
63
+
);
64
}
65
}
+148
-14
src/views/standardfeed.ts
···
1
import { getSubscribedPublications } from "lib/standardsite";
2
import ATmarkPlugin from "main";
3
-
import { ItemView, WorkspaceLeaf } from "obsidian";
4
-
import { SiteStandardPublication } from "@atcute/standard-site";
0
5
import { ATRecord } from "lib";
0
0
6
7
export const VIEW_STANDARD_FEED = "standard-site-feed";
8
···
36
container.addClass("standard-site-view");
37
this.renderHeader(container);
38
39
-
const loading = container.createEl("p", { text: "Loading feed..." });
40
0
41
try {
42
const pubs = await getSubscribedPublications(this.plugin.client, this.plugin.settings.identifier);
43
loading.remove();
44
45
if (pubs.length === 0) {
46
-
container.createEl("p", { text: "No subscriptions found. Subscribe to publications first." });
47
return;
48
}
49
50
const list = container.createEl("div", { cls: "standard-site-list" });
51
52
for (const pub of pubs) {
53
-
this.renderPublicationCard(list, pub);
54
}
55
} catch (error) {
56
-
loading.remove();
57
const message = error instanceof Error ? error.message : String(error);
0
58
container.createEl("p", { text: `Failed to load feed: ${message}`, cls: "standard-site-error" });
0
0
59
}
60
}
61
62
-
private renderPublicationCard(container: HTMLElement, pub: ATRecord<SiteStandardPublication.Main>) {
63
const card = container.createEl("div", { cls: "standard-site-publication" });
64
65
-
// Header with name
66
const header = card.createEl("div", { cls: "standard-site-publication-header" });
67
header.createEl("h3", {
68
text: pub.value.name,
69
cls: "standard-site-publication-name"
70
});
0
0
0
0
0
0
71
72
-
// Body
73
const body = card.createEl("div", { cls: "standard-site-publication-body" });
74
75
-
// URL
0
0
0
0
0
0
0
0
0
0
0
0
0
76
const urlLine = body.createEl("div", { cls: "standard-site-publication-url" });
77
const link = urlLine.createEl("a", { text: pub.value.url, href: pub.value.url });
78
link.setAttr("target", "_blank");
79
80
-
// Description
81
if (pub.value.description) {
82
body.createEl("p", {
83
text: pub.value.description,
···
85
});
86
}
87
88
-
// Make card clickable
89
card.addClass("clickable");
90
card.addEventListener("click", (e) => {
91
-
// Don't trigger if clicking the link
92
if ((e.target as HTMLElement).tagName !== "A") {
93
-
window.open(pub.value.url, "_blank");
94
}
95
});
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
96
}
97
98
renderHeader(container: HTMLElement) {
···
1
import { getSubscribedPublications } from "lib/standardsite";
2
import ATmarkPlugin from "main";
3
+
import { ItemView, Notice, WorkspaceLeaf, setIcon } from "obsidian";
4
+
import { Main as Document } from "@atcute/standard-site/types/document";
5
+
import { Main as Publication } from "@atcute/standard-site/types/publication";
6
import { ATRecord } from "lib";
7
+
import { parseResourceUri } from "@atcute/lexicons";
8
+
import { getPublicationDocuments } from "lib/standardsite";
9
10
export const VIEW_STANDARD_FEED = "standard-site-feed";
11
···
39
container.addClass("standard-site-view");
40
this.renderHeader(container);
41
0
42
43
+
const loading = container.createEl("p", { text: "Loading feed..." });
44
try {
45
const pubs = await getSubscribedPublications(this.plugin.client, this.plugin.settings.identifier);
46
loading.remove();
47
48
if (pubs.length === 0) {
49
+
container.createEl("p", { text: "No subscriptions found" });
50
return;
51
}
52
53
const list = container.createEl("div", { cls: "standard-site-list" });
54
55
for (const pub of pubs) {
56
+
void this.renderPublicationCard(list, pub);
57
}
58
} catch (error) {
0
59
const message = error instanceof Error ? error.message : String(error);
60
+
console.error("Failed to load feed:", error);
61
container.createEl("p", { text: `Failed to load feed: ${message}`, cls: "standard-site-error" });
62
+
} finally {
63
+
loading.remove();
64
}
65
}
66
67
+
private async renderPublicationCard(container: HTMLElement, pub: ATRecord<Publication>) {
68
const card = container.createEl("div", { cls: "standard-site-publication" });
69
0
70
const header = card.createEl("div", { cls: "standard-site-publication-header" });
71
header.createEl("h3", {
72
text: pub.value.name,
73
cls: "standard-site-publication-name"
74
});
75
+
const extLink = header.createEl("span", { cls: "clickable standard-site-publication-external" });
76
+
setIcon(extLink, "external-link");
77
+
extLink.addEventListener("click", (e) => {
78
+
e.stopPropagation();
79
+
window.open(pub.value.url, "_blank");
80
+
});
81
0
82
const body = card.createEl("div", { cls: "standard-site-publication-body" });
83
84
+
const handleEl = body.createEl("span", { cls: "standard-site-author-handle", text: "..." });
85
+
const parsed = parseResourceUri(pub.uri);
86
+
if (parsed.ok) {
87
+
this.plugin.client.getActor(parsed.value.repo).then(actor => {
88
+
if (actor?.handle) {
89
+
handleEl.setText(`@${actor.handle}`);
90
+
} else {
91
+
handleEl.setText("");
92
+
}
93
+
}).catch(() => {
94
+
handleEl.setText("");
95
+
});
96
+
}
97
+
98
const urlLine = body.createEl("div", { cls: "standard-site-publication-url" });
99
const link = urlLine.createEl("a", { text: pub.value.url, href: pub.value.url });
100
link.setAttr("target", "_blank");
101
0
102
if (pub.value.description) {
103
body.createEl("p", {
104
text: pub.value.description,
···
106
});
107
}
108
0
109
card.addClass("clickable");
110
card.addEventListener("click", (e) => {
0
111
if ((e.target as HTMLElement).tagName !== "A") {
112
+
void this.renderPublicationDocuments(pub);
113
}
114
});
115
+
}
116
+
117
+
private async renderPublicationDocuments(pub: ATRecord<Publication>) {
118
+
const container = this.contentEl;
119
+
container.empty();
120
+
container.addClass("standard-site-view");
121
+
122
+
const header = container.createEl("div", { cls: "standard-site-header" });
123
+
const backBtn = header.createEl("span", { text: "Back", cls: "clickable standard-site-back" });
124
+
setIcon(backBtn, "arrow-left");
125
+
backBtn.addEventListener("click", () => {
126
+
void this.render();
127
+
});
128
+
129
+
const titleGroup = header.createEl("div", { cls: "standard-site-title-group" });
130
+
titleGroup.createEl("h2", { text: pub.value.name });
131
+
const handleEl = titleGroup.createEl("span", { cls: "standard-site-author-handle", text: "..." });
132
+
133
+
const parsed = parseResourceUri(pub.uri);
134
+
if (!parsed.ok) {
135
+
// This is the name of the plugin, which contains the acronym "AT"
136
+
// eslint-disable-next-line obsidianmd/ui/sentence-case
137
+
container.createEl("p", { text: "Failed to parse publication URI." });
138
+
console.error("Failed to parse publication URI:", parsed.error);
139
+
140
+
return;
141
+
}
142
+
143
+
// Fetch actor handle asynchronously without blocking document load
144
+
this.plugin.client.getActor(parsed.value.repo).then(actor => {
145
+
if (actor?.handle) {
146
+
handleEl.setText(`@${actor.handle}`);
147
+
} else {
148
+
handleEl.setText("");
149
+
}
150
+
}).catch(() => {
151
+
handleEl.setText("");
152
+
});
153
+
154
+
const loading = container.createEl("p", { text: "Loading documents..." });
155
+
156
+
try {
157
+
const docsResp = await getPublicationDocuments(this.plugin.client, parsed.value.repo, pub.uri);
158
+
loading.remove();
159
+
160
+
if (docsResp.records.length === 0) {
161
+
container.createEl("p", { text: "No documents found for this publication." });
162
+
return;
163
+
}
164
+
165
+
const list = container.createEl("div", { cls: "standard-site-list" });
166
+
for (const doc of docsResp.records) {
167
+
this.renderDocumentCard(list, doc, pub);
168
+
}
169
+
} catch (error) {
170
+
loading.remove();
171
+
const message = error instanceof Error ? error.message : String(error);
172
+
container.createEl("p", { text: `Failed to load documents: ${message}`, cls: "standard-site-error" });
173
+
}
174
+
}
175
+
176
+
177
+
private renderDocumentCard(container: HTMLElement, doc: ATRecord<Document>, pub: ATRecord<Publication>) {
178
+
const card = container.createEl("div", { cls: "standard-site-document" });
179
+
180
+
const header = card.createEl("div", { cls: "standard-site-document-header" });
181
+
header.createEl("h3", { text: doc.value.title, cls: "standard-site-document-title" });
182
+
183
+
let clipIcon = "book-open";
184
+
if (this.plugin.clipper.existsInClipDir(doc)) {
185
+
clipIcon = "book-open-check";
186
+
}
187
+
const clipBtn = header.createEl("span", { cls: "clickable standard-site-document-clip" });
188
+
setIcon(clipBtn, clipIcon);
189
+
clipBtn.addEventListener("click", (e) => {
190
+
e.stopPropagation();
191
+
try {
192
+
void this.plugin.clipper.clipDocument(doc, pub);
193
+
} catch (error) {
194
+
const message = error instanceof Error ? error.message : String(error);
195
+
new Notice(`Failed to clip document: ${message}`);
196
+
console.error("Failed to clip document:", error);
197
+
}
198
+
})
199
+
200
+
201
+
if (doc.value.path) {
202
+
const extLink = header.createEl("span", { cls: "clickable standard-site-document-external" });
203
+
setIcon(extLink, "external-link");
204
+
const baseUrl = pub.value.url.replace(/\/+$/, "");
205
+
const path = doc.value.path.startsWith("/") ? doc.value.path : `/${doc.value.path}`;
206
+
extLink.addEventListener("click", (e) => {
207
+
e.stopPropagation();
208
+
window.open(`${baseUrl}${path}`, "_blank");
209
+
});
210
+
}
211
+
212
+
const body = card.createEl("div", { cls: "standard-site-document-body" });
213
+
214
+
if (doc.value.description) {
215
+
body.createEl("p", { text: doc.value.description, cls: "standard-site-document-description" });
216
+
}
217
+
218
+
if (doc.value.tags && doc.value.tags.length > 0) {
219
+
const tags = body.createEl("div", { cls: "standard-site-document-tags" });
220
+
for (const tag of doc.value.tags) {
221
+
tags.createEl("span", { text: tag, cls: "standard-site-document-tag" });
222
+
}
223
+
}
224
+
225
+
if (doc.value.publishedAt) {
226
+
const footer = card.createEl("div", { cls: "standard-site-document-footer" });
227
+
const date = new Date(doc.value.publishedAt).toLocaleDateString();
228
+
footer.createEl("span", { text: date, cls: "standard-site-document-date" });
229
+
}
230
}
231
232
renderHeader(container: HTMLElement) {
+147
styles.css
···
1207
.standard-site-error {
1208
color: var(--text-error);
1209
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1207
.standard-site-error {
1208
color: var(--text-error);
1209
}
1210
+
1211
+
/* Standard Site Documents */
1212
+
.standard-site-document {
1213
+
background: var(--background-secondary);
1214
+
border: 1px solid var(--background-modifier-border);
1215
+
border-radius: var(--radius-m);
1216
+
padding: 16px;
1217
+
display: flex;
1218
+
flex-direction: column;
1219
+
transition: box-shadow 0.15s ease, border-color 0.15s ease;
1220
+
}
1221
+
1222
+
.standard-site-document:hover {
1223
+
box-shadow: var(--shadow-s);
1224
+
border-color: var(--background-modifier-border-hover);
1225
+
}
1226
+
1227
+
.standard-site-document-header {
1228
+
display: flex;
1229
+
align-items: flex-start;
1230
+
justify-content: space-between;
1231
+
gap: 8px;
1232
+
margin-bottom: 8px;
1233
+
}
1234
+
1235
+
.standard-site-document-title {
1236
+
margin: 0;
1237
+
font-size: var(--h3-size);
1238
+
font-weight: var(--font-semibold);
1239
+
color: var(--text-normal);
1240
+
flex: 1;
1241
+
line-height: 1.3;
1242
+
}
1243
+
1244
+
.standard-site-document-external {
1245
+
display: flex;
1246
+
align-items: center;
1247
+
justify-content: center;
1248
+
flex-shrink: 0;
1249
+
width: 24px;
1250
+
height: 24px;
1251
+
border-radius: var(--radius-s);
1252
+
color: var(--text-faint);
1253
+
transition: all 0.15s ease;
1254
+
}
1255
+
1256
+
.standard-site-document-external:hover {
1257
+
background: var(--background-modifier-hover);
1258
+
color: var(--text-normal);
1259
+
}
1260
+
1261
+
.standard-site-document-external svg {
1262
+
width: 14px;
1263
+
height: 14px;
1264
+
}
1265
+
1266
+
.standard-site-document-body {
1267
+
display: flex;
1268
+
flex-direction: column;
1269
+
gap: 8px;
1270
+
}
1271
+
1272
+
.standard-site-document-description {
1273
+
margin: 0;
1274
+
color: var(--text-muted);
1275
+
font-size: var(--font-small);
1276
+
line-height: var(--line-height-normal);
1277
+
display: -webkit-box;
1278
+
-webkit-line-clamp: 3;
1279
+
-webkit-box-orient: vertical;
1280
+
overflow: hidden;
1281
+
}
1282
+
1283
+
.standard-site-document-tags {
1284
+
display: flex;
1285
+
flex-wrap: wrap;
1286
+
gap: 6px;
1287
+
}
1288
+
1289
+
.standard-site-document-tag {
1290
+
font-size: var(--font-smallest);
1291
+
padding: 2px 8px;
1292
+
border-radius: var(--radius-s);
1293
+
background: var(--background-modifier-border);
1294
+
color: var(--text-muted);
1295
+
border: 1px solid var(--background-modifier-border-hover);
1296
+
}
1297
+
1298
+
.standard-site-document-footer {
1299
+
display: flex;
1300
+
align-items: center;
1301
+
margin-top: 12px;
1302
+
padding-top: 8px;
1303
+
border-top: 1px solid var(--background-modifier-border);
1304
+
}
1305
+
1306
+
.standard-site-document-date {
1307
+
font-size: var(--font-smallest);
1308
+
color: var(--text-faint);
1309
+
}
1310
+
1311
+
.standard-site-title-group {
1312
+
display: flex;
1313
+
flex-direction: column;
1314
+
flex: 1;
1315
+
min-width: 0;
1316
+
}
1317
+
1318
+
.standard-site-author-handle {
1319
+
font-size: var(--font-small);
1320
+
color: var(--text-muted);
1321
+
}
1322
+
1323
+
.standard-site-back {
1324
+
font-size: var(--font-small);
1325
+
color: var(--text-muted);
1326
+
padding: 4px 8px;
1327
+
border-radius: var(--radius-s);
1328
+
transition: all 0.15s ease;
1329
+
}
1330
+
1331
+
.standard-site-back:hover {
1332
+
background: var(--background-modifier-hover);
1333
+
color: var(--text-normal);
1334
+
}
1335
+
1336
+
.standard-site-publication-external {
1337
+
display: flex;
1338
+
align-items: center;
1339
+
justify-content: center;
1340
+
flex-shrink: 0;
1341
+
width: 24px;
1342
+
height: 24px;
1343
+
border-radius: var(--radius-s);
1344
+
color: var(--text-faint);
1345
+
transition: all 0.15s ease;
1346
+
}
1347
+
1348
+
.standard-site-publication-external:hover {
1349
+
background: var(--background-modifier-hover);
1350
+
color: var(--text-normal);
1351
+
}
1352
+
1353
+
.standard-site-publication-external svg {
1354
+
width: 14px;
1355
+
height: 14px;
1356
+
}