@dmthepm/commune 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +468 -0
  3. package/bin/commune.mjs +29 -0
  4. package/lib/cli/check.d.ts +13 -0
  5. package/lib/cli/check.js +58 -0
  6. package/lib/cli/errors.d.ts +29 -0
  7. package/lib/cli/errors.js +41 -0
  8. package/lib/cli/gate.d.ts +34 -0
  9. package/lib/cli/gate.js +165 -0
  10. package/lib/cli/main.d.ts +20 -0
  11. package/lib/cli/main.js +175 -0
  12. package/lib/cli/query.d.ts +30 -0
  13. package/lib/cli/query.js +103 -0
  14. package/lib/cli/related.d.ts +17 -0
  15. package/lib/cli/related.js +177 -0
  16. package/lib/cli/render.d.ts +20 -0
  17. package/lib/cli/render.js +32 -0
  18. package/lib/cli/root.d.ts +11 -0
  19. package/lib/cli/root.js +28 -0
  20. package/lib/cli/usage.d.ts +3 -0
  21. package/lib/cli/usage.js +46 -0
  22. package/lib/cli/version.d.ts +24 -0
  23. package/lib/cli/version.js +29 -0
  24. package/lib/integration.d.ts +24 -0
  25. package/lib/integration.js +111 -0
  26. package/lib/lib/graph.d.ts +354 -0
  27. package/lib/lib/graph.js +774 -0
  28. package/lib/markdown.d.ts +30 -0
  29. package/lib/markdown.js +24 -0
  30. package/lib/rehype-external-links.d.ts +15 -0
  31. package/lib/rehype-external-links.js +46 -0
  32. package/lib/remark-wikilinks.d.ts +25 -0
  33. package/lib/remark-wikilinks.js +108 -0
  34. package/package.json +101 -0
  35. package/src/components/Backlinks.astro +17 -0
  36. package/src/components/BacklinksScript.astro +117 -0
  37. package/src/components/Footer.astro +35 -0
  38. package/src/components/Header.astro +250 -0
  39. package/src/components/HeaderStarScript.astro +380 -0
  40. package/src/components/HomeFooterCards.astro +155 -0
  41. package/src/components/MarkdownLink.astro +36 -0
  42. package/src/components/PlausibleScript.astro +13 -0
  43. package/src/components/RelatedNotes.astro +77 -0
  44. package/src/components/SearchModal.astro +213 -0
  45. package/src/components/StarredLinksScript.astro +92 -0
  46. package/src/components/panes.ts +61 -0
  47. package/src/styles/design-system.css +157 -0
  48. package/src/styles/notes.css +85 -0
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The markdown processor, assembled once.
3
+ *
4
+ * Astro 7 renders markdown with Sätteri and no longer installs the unified
5
+ * pipeline, so a consumer who wants `[[WikiLinks]]` has to build a processor
6
+ * and hand it to `markdown.processor`. That is three imports, an option shape
7
+ * and one ordering rule (the remark plugin turns wikilinks into links; the
8
+ * rehype plugin then decides which links are external) — all of it engine
9
+ * knowledge, none of it a decision a wiki's author should have to re-derive.
10
+ * This function is that assembly, so `astro.config.mjs` reads as one line.
11
+ *
12
+ * `site` is a parameter and not a constant because the engine has no host of
13
+ * its own: what counts as an external link is decided against the consumer's
14
+ * own origin, which lives once in their `defineConfig({ site })`.
15
+ */
16
+ import type { GraphOptions } from './lib/graph.ts';
17
+ export interface CommuneMarkdownOptions extends GraphOptions {
18
+ /** The site's own origin, exactly as `astro.config.mjs` declares `site`. */
19
+ site?: string | URL;
20
+ /**
21
+ * The project root, for resolving `[[WikiLinks]]` against its content tree.
22
+ *
23
+ * Inherited from `GraphOptions`, and almost never worth passing: it
24
+ * defaults to `process.cwd()`, which is also the default Astro resolves
25
+ * `root` to. Pass it when the build runs from somewhere other than the
26
+ * project directory — the same case `astro build --root` exists for.
27
+ */
28
+ root?: string;
29
+ }
30
+ export declare function communeMarkdown({ site, root }?: CommuneMarkdownOptions): import("@astrojs/markdown-remark").MarkdownProcessor<import("@astrojs/markdown-remark").UnifiedResolvedOptions>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The markdown processor, assembled once.
3
+ *
4
+ * Astro 7 renders markdown with Sätteri and no longer installs the unified
5
+ * pipeline, so a consumer who wants `[[WikiLinks]]` has to build a processor
6
+ * and hand it to `markdown.processor`. That is three imports, an option shape
7
+ * and one ordering rule (the remark plugin turns wikilinks into links; the
8
+ * rehype plugin then decides which links are external) — all of it engine
9
+ * knowledge, none of it a decision a wiki's author should have to re-derive.
10
+ * This function is that assembly, so `astro.config.mjs` reads as one line.
11
+ *
12
+ * `site` is a parameter and not a constant because the engine has no host of
13
+ * its own: what counts as an external link is decided against the consumer's
14
+ * own origin, which lives once in their `defineConfig({ site })`.
15
+ */
16
+ import { unified } from '@astrojs/markdown-remark';
17
+ import remarkWikiLinks from "./remark-wikilinks.js";
18
+ import rehypeExternalLinks from "./rehype-external-links.js";
19
+ export function communeMarkdown({ site, root } = {}) {
20
+ return unified({
21
+ remarkPlugins: [[remarkWikiLinks, { root }]],
22
+ rehypePlugins: [[rehypeExternalLinks, { site }]],
23
+ });
24
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Rehype plugin to open every external link in a new tab.
3
+ *
4
+ * The design system marks `a[target="_blank"]` with an arrow, so setting the
5
+ * attribute here is what makes that marker automatic for plain markdown links.
6
+ */
7
+ import type { Root } from 'hast';
8
+ export interface ExternalLinksOptions {
9
+ /** The site's own origin, exactly as `astro.config.mjs` declares `site`. */
10
+ site?: string | URL;
11
+ }
12
+ /**
13
+ * Rehype plugin function
14
+ */
15
+ export default function rehypeExternalLinks({ site }?: ExternalLinksOptions): (tree: Root) => void;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Rehype plugin to open every external link in a new tab.
3
+ *
4
+ * The design system marks `a[target="_blank"]` with an arrow, so setting the
5
+ * attribute here is what makes that marker automatic for plain markdown links.
6
+ */
7
+ import { visit } from 'unist-util-visit';
8
+ /**
9
+ * Rehype plugin function
10
+ */
11
+ export default function rehypeExternalLinks({ site } = {}) {
12
+ // A link is internal because it points at the site's own host, and the
13
+ // engine has no host of its own: it comes from the Astro `site` config.
14
+ // Without one, nothing can be recognised as internal by hostname — only
15
+ // relative links stay untouched.
16
+ const siteHost = site ? new URL(site).hostname : null;
17
+ return function transformer(tree) {
18
+ visit(tree, 'element', (node) => {
19
+ if (node.tagName !== 'a')
20
+ return;
21
+ // A link that already declares `target` was written by hand — raw
22
+ // HTML in a note, or a component — and owns its own `rel`.
23
+ // Overwriting it would drop attributes its author chose.
24
+ if (node.properties?.target !== undefined)
25
+ return;
26
+ const href = node.properties?.href;
27
+ if (typeof href !== 'string')
28
+ return;
29
+ // Wikilinks and internal links are relative, so they have no
30
+ // protocol, never match here, and are left alone.
31
+ if (!href.startsWith('http://') && !href.startsWith('https://'))
32
+ return;
33
+ let hostname;
34
+ try {
35
+ hostname = new URL(href).hostname;
36
+ }
37
+ catch {
38
+ return;
39
+ }
40
+ if (hostname === siteHost)
41
+ return;
42
+ node.properties.target = '_blank';
43
+ node.properties.rel = ['noopener', 'noreferrer'];
44
+ });
45
+ };
46
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Remark plugin to transform WikiLinks [[Note Title]] into proper markdown links.
3
+ *
4
+ * Transforms:
5
+ * [[Atomic Notes]] → [Atomic Notes](/notes/atomic-notes/)
6
+ * [[Note Title|Display Text]] → [Display Text](/notes/note-title/)
7
+ *
8
+ * This runs at build time during markdown compilation, before HTML generation.
9
+ * Which content exists, and the URL each piece lives at, is decided by the
10
+ * graph core in `src/lib/graph.ts` — not here.
11
+ */
12
+ import type { Root } from 'mdast';
13
+ import { type GraphOptions } from './lib/graph.ts';
14
+ export type WikiLinksOptions = GraphOptions;
15
+ /**
16
+ * Remark plugin function.
17
+ *
18
+ * Takes the project root rather than assuming one, because the plugin resolves
19
+ * links against the content tree of whichever project is being built — which,
20
+ * once this is a package in somebody else's `node_modules`, is not the
21
+ * directory this file lives in. Omitting it falls back to `process.cwd()`,
22
+ * which is also Astro's own default for `root`, so a consumer who does not
23
+ * pass one gets the project they ran `astro build` in.
24
+ */
25
+ export default function remarkWikiLinks({ root }?: WikiLinksOptions): (tree: Root) => Promise<void>;
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Remark plugin to transform WikiLinks [[Note Title]] into proper markdown links.
3
+ *
4
+ * Transforms:
5
+ * [[Atomic Notes]] → [Atomic Notes](/notes/atomic-notes/)
6
+ * [[Note Title|Display Text]] → [Display Text](/notes/note-title/)
7
+ *
8
+ * This runs at build time during markdown compilation, before HTML generation.
9
+ * Which content exists, and the URL each piece lives at, is decided by the
10
+ * graph core in `src/lib/graph.ts` — not here.
11
+ */
12
+ import { visit } from 'unist-util-visit';
13
+ import { getLinkLookup } from "./lib/graph.js";
14
+ /**
15
+ * Remark plugin function.
16
+ *
17
+ * Takes the project root rather than assuming one, because the plugin resolves
18
+ * links against the content tree of whichever project is being built — which,
19
+ * once this is a package in somebody else's `node_modules`, is not the
20
+ * directory this file lives in. Omitting it falls back to `process.cwd()`,
21
+ * which is also Astro's own default for `root`, so a consumer who does not
22
+ * pass one gets the project they ran `astro build` in.
23
+ */
24
+ export default function remarkWikiLinks({ root } = {}) {
25
+ return async function transformer(tree) {
26
+ const lookup = await getLinkLookup({ root });
27
+ visit(tree, 'text', (node, index, parent) => {
28
+ if (!parent || index === undefined)
29
+ return;
30
+ const text = node.value;
31
+ const wikiLinkRegex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
32
+ // Check if this text node contains WikiLinks
33
+ if (!wikiLinkRegex.test(text))
34
+ return;
35
+ // Reset regex
36
+ wikiLinkRegex.lastIndex = 0;
37
+ const newNodes = [];
38
+ let lastIndex = 0;
39
+ let match;
40
+ while ((match = wikiLinkRegex.exec(text)) !== null) {
41
+ const [fullMatch, linkText, displayText] = match;
42
+ const startIndex = match.index;
43
+ // Add text before the WikiLink
44
+ if (startIndex > lastIndex) {
45
+ newNodes.push({
46
+ type: 'text',
47
+ value: text.slice(lastIndex, startIndex),
48
+ });
49
+ }
50
+ // Resolve WikiLink to URL path
51
+ const trimmedLinkText = linkText.trim();
52
+ const lookupKey = trimmedLinkText.toLowerCase();
53
+ const resolved = lookup.get(lookupKey);
54
+ if (resolved) {
55
+ // Create a proper link node with correct URL for collection
56
+ // Add data-collection attribute to identify research links for styling
57
+ const linkNode = {
58
+ type: 'link',
59
+ url: resolved.urlPath,
60
+ children: [
61
+ {
62
+ type: 'text',
63
+ value: displayText?.trim() || trimmedLinkText,
64
+ },
65
+ ],
66
+ };
67
+ // Add data attribute for research links (will be used for new tab behavior)
68
+ if (resolved.collection === 'research') {
69
+ linkNode.data = {
70
+ hProperties: {
71
+ 'data-collection': 'research',
72
+ 'class': 'wikilink research-link'
73
+ }
74
+ };
75
+ }
76
+ else {
77
+ linkNode.data = {
78
+ hProperties: {
79
+ 'class': 'wikilink'
80
+ }
81
+ };
82
+ }
83
+ newNodes.push(linkNode);
84
+ }
85
+ else {
86
+ // Leave unresolved WikiLinks as plain text (without brackets)
87
+ // These are notes that don't exist yet in the public wiki
88
+ newNodes.push({
89
+ type: 'text',
90
+ value: displayText?.trim() || trimmedLinkText,
91
+ });
92
+ }
93
+ lastIndex = startIndex + fullMatch.length;
94
+ }
95
+ // Add remaining text
96
+ if (lastIndex < text.length) {
97
+ newNodes.push({
98
+ type: 'text',
99
+ value: text.slice(lastIndex),
100
+ });
101
+ }
102
+ // Replace the text node with our transformed nodes
103
+ if (newNodes.length > 0) {
104
+ parent.children.splice(index, 1, ...newNodes);
105
+ }
106
+ });
107
+ };
108
+ }
package/package.json ADDED
@@ -0,0 +1,101 @@
1
+ {
2
+ "name": "@dmthepm/commune",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "An Astro wiki engine with WikiLinks, sliding panes, backlinks and static search, plus a commune CLI that queries the content graph and checks links",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "astro",
9
+ "astro-integration",
10
+ "withastro",
11
+ "wiki",
12
+ "wikilinks",
13
+ "backlinks"
14
+ ],
15
+ "bin": {
16
+ "commune": "bin/commune.mjs"
17
+ },
18
+ "exports": {
19
+ "./graph": {
20
+ "types": "./lib/lib/graph.d.ts",
21
+ "default": "./lib/lib/graph.js"
22
+ },
23
+ "./astro": {
24
+ "types": "./lib/integration.d.ts",
25
+ "default": "./lib/integration.js"
26
+ },
27
+ "./markdown": {
28
+ "types": "./lib/markdown.d.ts",
29
+ "default": "./lib/markdown.js"
30
+ },
31
+ "./remark": {
32
+ "types": "./lib/remark-wikilinks.d.ts",
33
+ "default": "./lib/remark-wikilinks.js"
34
+ },
35
+ "./rehype": {
36
+ "types": "./lib/rehype-external-links.d.ts",
37
+ "default": "./lib/rehype-external-links.js"
38
+ },
39
+ "./components/*": "./src/components/*",
40
+ "./styles/*": "./src/styles/*",
41
+ "./package.json": "./package.json"
42
+ },
43
+ "files": [
44
+ "bin",
45
+ "lib",
46
+ "src/components",
47
+ "src/styles",
48
+ "LICENSE",
49
+ "README.md"
50
+ ],
51
+ "engines": {
52
+ "node": ">=22.12.0"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "git+https://github.com/dmthepm/commune-wiki.git"
60
+ },
61
+ "bugs": {
62
+ "url": "https://github.com/dmthepm/commune-wiki/issues"
63
+ },
64
+ "homepage": "https://github.com/dmthepm/commune-wiki#readme",
65
+ "scripts": {
66
+ "build:lib": "tsc -p tsconfig.build.json",
67
+ "prepare": "pnpm build:lib",
68
+ "pretest": "pnpm build:lib",
69
+ "dev": "astro dev",
70
+ "start": "astro dev",
71
+ "build": "pnpm build:lib && astro build && node bin/commune.mjs gate",
72
+ "test": "node --test \"tests/**/*.test.mjs\"",
73
+ "test:consumer": "pnpm --dir tests/fixtures/consumer install && pnpm --dir tests/fixtures/consumer build",
74
+ "preview": "astro preview",
75
+ "astro": "astro"
76
+ },
77
+ "peerDependencies": {
78
+ "@astrojs/markdown-remark": "^7.3.0",
79
+ "astro": "^7.0.0"
80
+ },
81
+ "dependencies": {
82
+ "github-slugger": "^2.0.0",
83
+ "globby": "^14.0.0",
84
+ "gray-matter": "^4.0.3",
85
+ "unist-util-visit": "^5.0.0"
86
+ },
87
+ "devDependencies": {
88
+ "@astrojs/check": "^0.9.0",
89
+ "@astrojs/markdown-remark": "^7.3.0",
90
+ "@astrojs/sitemap": "^3.7.4",
91
+ "@tailwindcss/typography": "^0.5.10",
92
+ "@types/hast": "^3.0.5",
93
+ "@types/mdast": "^4.0.4",
94
+ "@types/node": "^22.20.1",
95
+ "astro": "^7.2.10",
96
+ "autoprefixer": "^10.4.16",
97
+ "puppeteer": "^24.25.0",
98
+ "tailwindcss": "^3.4.0",
99
+ "typescript": "^5.6.0"
100
+ }
101
+ }
@@ -0,0 +1,17 @@
1
+ ---
2
+ export interface Props { slug: string; title?: string; hideCount?: boolean; }
3
+ const { slug, hideCount = false } = Astro.props;
4
+ ---
5
+ <aside class="backlinks" data-backlinks-for={slug} data-hide-count={hideCount ? 'true' : undefined}>
6
+ <h4>Links to this note</h4>
7
+ <ul class="backlinks-list"></ul>
8
+ </aside>
9
+
10
+ <style>
11
+ .backlinks{margin-top:3rem;padding-top:1.2rem;border-top:1px solid var(--c-border)}
12
+ .backlinks.hidden{display:none}
13
+ .backlinks h4{font-size:.85rem;color:var(--c-text-muted);margin:0 0 .8rem;font-weight:500}
14
+ .backlinks li{margin:.4rem 0;font-size:.9rem}
15
+ .backlinks a{opacity:.85}
16
+ .backlinks a:hover{opacity:1}
17
+ </style>
@@ -0,0 +1,117 @@
1
+ <script is:inline>
2
+ // === BACKLINKS INITIALIZATION ===
3
+ // Handles dynamic backlinks loading for both static and dynamically loaded panes
4
+
5
+ (function() {
6
+ let backlinksData = null;
7
+
8
+ async function initializeBacklinks(scope = document) {
9
+ try {
10
+ // Fetch backlinks data if not already loaded
11
+ if (!backlinksData) {
12
+ const res = await fetch('/backlinks.json');
13
+ if (res.ok) {
14
+ backlinksData = await res.json();
15
+ } else {
16
+ console.warn('Failed to load backlinks.json');
17
+ return;
18
+ }
19
+ }
20
+
21
+ // Find all backlinks sections within the scope
22
+ const backlinksSections = scope.querySelectorAll('.backlinks');
23
+
24
+ backlinksSections.forEach(section => {
25
+ const slug = section.dataset.backlinksFor;
26
+ if (!slug) return;
27
+
28
+ const list = section.querySelector('.backlinks-list');
29
+ if (!list) return;
30
+
31
+ // Get backlinks for this slug
32
+ const here = slug.endsWith('/') ? slug : slug + '/';
33
+ const rec = backlinksData?.[here];
34
+ const items = rec?.inbound ?? [];
35
+
36
+ if (!items.length) {
37
+ // Hide the entire section if no backlinks
38
+ section.classList.add('hidden');
39
+ return;
40
+ }
41
+
42
+ // Show the section and populate links
43
+ section.classList.remove('hidden');
44
+
45
+ // Update heading with count (unless hideCount attribute is present)
46
+ const heading = section.querySelector('h4');
47
+ const hideCount = section.hasAttribute('data-hide-count');
48
+ if (heading && !hideCount) {
49
+ const count = items.length;
50
+ const linkText = count === 1 ? 'link' : 'links';
51
+ heading.textContent = `${count} ${linkText} to this note`;
52
+ }
53
+
54
+ const titleFor = (u) => {
55
+ const note = backlinksData[u];
56
+ return note?.title || u.split('/').filter(Boolean).pop()?.replace(/-/g,' ');
57
+ };
58
+
59
+ // Render links and then add stars as separate clickable elements
60
+ list.innerHTML = items.map(s => `<li><a href="${s}">${titleFor(s)}</a></li>`).join('');
61
+
62
+ // Add stars to starred notes
63
+ list.querySelectorAll('li').forEach((li, index) => {
64
+ const url = items[index];
65
+ const note = backlinksData[url];
66
+
67
+ if (note?.isStarred && !li.querySelector('.star-indicator-inline')) {
68
+ const link = li.querySelector('a');
69
+ if (link) {
70
+ const star = document.createElement('span');
71
+ star.className = 'star-indicator-inline';
72
+ star.textContent = ' ⭐';
73
+ star.setAttribute('aria-label', 'Top 5% most linked - click to learn more');
74
+ star.setAttribute('role', 'button');
75
+ star.setAttribute('tabindex', '0');
76
+
77
+ // Click handler to open modal
78
+ star.addEventListener('click', (e) => {
79
+ e.preventDefault();
80
+ e.stopPropagation();
81
+ if (typeof window.showStarModal === 'function') {
82
+ window.showStarModal();
83
+ }
84
+ });
85
+
86
+ // Keyboard support
87
+ star.addEventListener('keydown', (e) => {
88
+ if (e.key === 'Enter' || e.key === ' ') {
89
+ e.preventDefault();
90
+ e.stopPropagation();
91
+ if (typeof window.showStarModal === 'function') {
92
+ window.showStarModal();
93
+ }
94
+ }
95
+ });
96
+
97
+ link.insertAdjacentElement('afterend', star);
98
+ }
99
+ }
100
+ });
101
+ });
102
+ } catch (error) {
103
+ console.error('Error initializing backlinks:', error);
104
+ }
105
+ }
106
+
107
+ // Make it globally available for dynamic pane loading
108
+ window.initializeBacklinks = initializeBacklinks;
109
+
110
+ // Auto-initialize on DOM ready
111
+ if (document.readyState === 'loading') {
112
+ document.addEventListener('DOMContentLoaded', () => initializeBacklinks());
113
+ } else {
114
+ initializeBacklinks();
115
+ }
116
+ })();
117
+ </script>
@@ -0,0 +1,35 @@
1
+ ---
2
+ // Footer component with "Powered by Commune" link
3
+ ---
4
+ <footer class="site-footer">
5
+ <p>Powered by <a href="/notes/what-is-commune/">Commune</a></p>
6
+ </footer>
7
+
8
+ <style>
9
+ .site-footer {
10
+ margin-top: 3rem;
11
+ padding-top: 1.5rem;
12
+ border-top: 1px solid var(--c-border);
13
+ text-align: center;
14
+ }
15
+
16
+ .site-footer p {
17
+ margin: 0;
18
+ font-size: 0.9rem;
19
+ color: var(--c-text-muted);
20
+ }
21
+
22
+ .site-footer a {
23
+ color: var(--c-accent);
24
+ text-decoration: none;
25
+ border-bottom: 1px solid var(--c-accent);
26
+ padding-bottom: 1px;
27
+ transition: all 0.15s ease;
28
+ }
29
+
30
+ .site-footer a:hover {
31
+ color: var(--c-accent-hover);
32
+ background: var(--c-accent-soft);
33
+ border-bottom-color: var(--c-accent-hover);
34
+ }
35
+ </style>