@mh-gg/base-plugins 0.1.1-alpha.20260613T085325975Z
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.
- package/README.md +247 -0
- package/package.json +48 -0
- package/src/approvals/index.cjs +65 -0
- package/src/attachments/index.cjs +75 -0
- package/src/calendar/index.cjs +48 -0
- package/src/checklists/index.cjs +58 -0
- package/src/comments/index.cjs +15 -0
- package/src/comments/plugin.cjs +66 -0
- package/src/comments/reducer.cjs +81 -0
- package/src/comments/schemas.cjs +60 -0
- package/src/comments/state.cjs +17 -0
- package/src/comments/threads.cjs +44 -0
- package/src/comments/views.cjs +27 -0
- package/src/composer/capabilities.cjs +19 -0
- package/src/composer/compose.cjs +37 -0
- package/src/composer/index.cjs +15 -0
- package/src/composer/operations.cjs +42 -0
- package/src/composer/registry.cjs +155 -0
- package/src/composer/selection.cjs +39 -0
- package/src/composer/suite.cjs +32 -0
- package/src/crdt/client.mjs +207 -0
- package/src/crdt/index.cjs +258 -0
- package/src/embeds/index.cjs +90 -0
- package/src/files/index.cjs +133 -0
- package/src/index.cjs +19 -0
- package/src/labels/index.cjs +46 -0
- package/src/location-pins/index.cjs +142 -0
- package/src/markdown/documents/index.cjs +128 -0
- package/src/markdown/index.cjs +8 -0
- package/src/markdown/parser/index.cjs +127 -0
- package/src/markdown/providers/audio.cjs +77 -0
- package/src/markdown/providers/cloud.cjs +72 -0
- package/src/markdown/providers/developer.cjs +45 -0
- package/src/markdown/providers/direct.cjs +49 -0
- package/src/markdown/providers/games.cjs +26 -0
- package/src/markdown/providers/images.cjs +88 -0
- package/src/markdown/providers/index.cjs +97 -0
- package/src/markdown/providers/maps.cjs +24 -0
- package/src/markdown/providers/productivity.cjs +30 -0
- package/src/markdown/providers/res-inspired.cjs +11 -0
- package/src/markdown/providers/social.cjs +33 -0
- package/src/markdown/providers/video.cjs +139 -0
- package/src/markdown/resolve.cjs +87 -0
- package/src/media-rooms/index.cjs +244 -0
- package/src/presence/index.cjs +193 -0
- package/src/reactions/index.cjs +47 -0
- package/src/screen-share/index.cjs +84 -0
- package/src/shared/constants.cjs +87 -0
- package/src/shared/embed.cjs +82 -0
- package/src/shared/index.cjs +20 -0
- package/src/shared/roles.cjs +5 -0
- package/src/shared/scopes.cjs +15 -0
- package/src/shared/url.cjs +32 -0
- package/src/shared/validation.cjs +31 -0
- package/test/composable-plugins.test.cjs +170 -0
- package/test/crdt-plugin.test.cjs +168 -0
- package/test/embed-autodetect-providers.test.cjs +138 -0
- package/test/markdown-media-workflow-plugins.test.cjs +201 -0
- package/test/markdown-parser-edge-cases.test.cjs +86 -0
- package/test/plugin-structure.test.cjs +69 -0
- package/test/shared-plugin-edges.test.cjs +207 -0
package/src/index.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
...require("./shared/constants.cjs"),
|
|
3
|
+
...require("./comments/index.cjs"),
|
|
4
|
+
...require("./presence/index.cjs"),
|
|
5
|
+
...require("./media-rooms/index.cjs"),
|
|
6
|
+
...require("./screen-share/index.cjs"),
|
|
7
|
+
...require("./markdown/index.cjs"),
|
|
8
|
+
...require("./embeds/index.cjs"),
|
|
9
|
+
...require("./attachments/index.cjs"),
|
|
10
|
+
...require("./files/index.cjs"),
|
|
11
|
+
...require("./reactions/index.cjs"),
|
|
12
|
+
...require("./labels/index.cjs"),
|
|
13
|
+
...require("./approvals/index.cjs"),
|
|
14
|
+
...require("./checklists/index.cjs"),
|
|
15
|
+
...require("./calendar/index.cjs"),
|
|
16
|
+
...require("./location-pins/index.cjs"),
|
|
17
|
+
...require("./crdt/index.cjs"),
|
|
18
|
+
...require("./composer/index.cjs")
|
|
19
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { LABELS_PLUGIN_ID, LABELS_PLUGIN_KEY } = require("../shared/constants.cjs");
|
|
2
|
+
const {
|
|
3
|
+
allow,
|
|
4
|
+
activity,
|
|
5
|
+
actorName,
|
|
6
|
+
array,
|
|
7
|
+
createOperationSchemaDescriptor,
|
|
8
|
+
defineHostPlugin,
|
|
9
|
+
deny,
|
|
10
|
+
entityId,
|
|
11
|
+
enumValue,
|
|
12
|
+
memberOrBetter,
|
|
13
|
+
moderatorOrBetter,
|
|
14
|
+
object,
|
|
15
|
+
optionalString,
|
|
16
|
+
readonlyState,
|
|
17
|
+
scopeKey,
|
|
18
|
+
scopePayload,
|
|
19
|
+
string,
|
|
20
|
+
BASE_PLUGIN_VERSION
|
|
21
|
+
} = require("../shared/index.cjs");
|
|
22
|
+
|
|
23
|
+
function labelPayload(payload) { const value = object(payload, "label payload"); return { name: string(value.name, "name", 80), color: optionalString(value.color, "color", 40) || "neutral", description: optionalString(value.description, "description", 240) }; }
|
|
24
|
+
function labelApplyPayload(payload) { const value = object(payload, "label apply payload"); return { ...scopePayload(value), labelId: string(value.labelId, "labelId") }; }
|
|
25
|
+
const labelsHostPlugin = defineHostPlugin({
|
|
26
|
+
id: LABELS_PLUGIN_ID,
|
|
27
|
+
version: BASE_PLUGIN_VERSION,
|
|
28
|
+
meta: { name: "Reusable Labels And Tags" },
|
|
29
|
+
capabilities: { requires: ["room.state", "room.roles"], provides: ["taxonomy.labels", "taxonomy.scoped-tags"] },
|
|
30
|
+
stateSchemaDescriptor: { plugin: LABELS_PLUGIN_ID, shape: ["labels", "assignments", "activity"] },
|
|
31
|
+
operationSchemaDescriptor: createOperationSchemaDescriptor(LABELS_PLUGIN_ID, BASE_PLUGIN_VERSION, { "label.create": { required: ["name"], optional: ["color", "description"], authorize: { roles: ["moderator"] } }, "label.apply": { required: ["scopeType", "scopeId", "labelId"], authorize: { roles: ["member"] } }, "label.remove": { required: ["scopeType", "scopeId", "labelId"], authorize: { roles: ["member"] } } }),
|
|
32
|
+
schemas: { state: { parse(value) { const state = object(value, "labels state"); object(state.labels, "labels"); object(state.assignments, "assignments"); array(state.activity, "activity"); return state; } }, operations: { "label.create": { parse: labelPayload }, "label.apply": { parse: labelApplyPayload }, "label.remove": { parse: labelApplyPayload } }, publicView: { parse: readonlyState }, queries: { labelsForScope: { parse: readonlyState }, labelDirectory: { parse: readonlyState } } },
|
|
33
|
+
async createInitialState() { return { labels: {}, assignments: {}, activity: [] }; },
|
|
34
|
+
authorize(_ctx, op) { if (op.type === "label.create") return moderatorOrBetter(op.actor) ? allow() : deny("Moderators only"); if (["label.apply", "label.remove"].includes(op.type)) return memberOrBetter(op.actor) ? allow() : deny("Members only"); return deny("Unsupported label operation"); },
|
|
35
|
+
async reduce(_ctx, state, op) {
|
|
36
|
+
if (op.type === "label.create") { const label = { id: entityId("label", op), ...op.payload, createdBy: op.actor.memberId, createdAt: op.createdAt }; return { ...state, labels: { ...state.labels, [label.id]: label }, activity: activity(state, op, `${actorName(op.actor)} created label ${label.name}`) }; }
|
|
37
|
+
const label = state.labels[op.payload.labelId]; if (!label) throw new Error(`Label ${op.payload.labelId} not found`);
|
|
38
|
+
const key = scopeKey(op.payload.scopeType, op.payload.scopeId); const current = new Set(state.assignments[key] || []);
|
|
39
|
+
if (op.type === "label.apply") current.add(label.id); else current.delete(label.id);
|
|
40
|
+
return { ...state, assignments: { ...state.assignments, [key]: [...current].sort() }, activity: activity(state, op, `${actorName(op.actor)} ${op.type === "label.apply" ? "applied" : "removed"} ${label.name}`) };
|
|
41
|
+
},
|
|
42
|
+
getPublicView(_ctx, state) { return state; },
|
|
43
|
+
queries: { labelDirectory(_ctx, state) { return Object.values(state.labels).sort((a, b) => a.name.localeCompare(b.name)); }, labelsForScope(_ctx, state, input = {}) { const scoped = scopePayload(input, "labelsForScope input"); const ids = state.assignments[scopeKey(scoped.scopeType, scoped.scopeId)] || []; return ids.map((id) => state.labels[id]).filter(Boolean); } }
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
module.exports = { LABELS_PLUGIN_ID, LABELS_PLUGIN_KEY, labelsHostPlugin };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const { LOCATION_PINS_PLUGIN_ID, LOCATION_PINS_PLUGIN_KEY } = require("../shared/constants.cjs");
|
|
2
|
+
const {
|
|
3
|
+
BASE_PLUGIN_VERSION,
|
|
4
|
+
activity,
|
|
5
|
+
actorName,
|
|
6
|
+
adminOrOwner,
|
|
7
|
+
allow,
|
|
8
|
+
array,
|
|
9
|
+
createOperationSchemaDescriptor,
|
|
10
|
+
defineHostPlugin,
|
|
11
|
+
deny,
|
|
12
|
+
memberOrBetter,
|
|
13
|
+
number,
|
|
14
|
+
object,
|
|
15
|
+
optionalString,
|
|
16
|
+
readonlyState,
|
|
17
|
+
scopeKey,
|
|
18
|
+
scopePayload,
|
|
19
|
+
string
|
|
20
|
+
} = require("../shared/index.cjs");
|
|
21
|
+
|
|
22
|
+
function clampNumber(value, min, max) {
|
|
23
|
+
return Math.max(min, Math.min(max, value));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function wrapLongitude(value) {
|
|
27
|
+
return ((((value + 180) % 360) + 360) % 360) - 180;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function normalizePin(value) {
|
|
31
|
+
const lat = number(value.lat, "lat");
|
|
32
|
+
const lng = number(value.lng, "lng");
|
|
33
|
+
const zoom = value.zoom === undefined ? 13 : number(value.zoom, "zoom");
|
|
34
|
+
return {
|
|
35
|
+
lat: clampNumber(lat, -85.05112878, 85.05112878),
|
|
36
|
+
lng: wrapLongitude(lng),
|
|
37
|
+
zoom: Math.round(clampNumber(zoom, 2, 18))
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function locationPinSetPayload(payload) {
|
|
42
|
+
const value = object(payload, "location.pin.set payload");
|
|
43
|
+
return {
|
|
44
|
+
...scopePayload(value),
|
|
45
|
+
...normalizePin(value),
|
|
46
|
+
label: optionalString(value.label, "label", 180),
|
|
47
|
+
address: optionalString(value.address, "address", 300)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function locationPinClearPayload(payload) {
|
|
52
|
+
const value = object(payload, "location.pin.clear payload");
|
|
53
|
+
return scopePayload(value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function locationPinsForScope(_ctx, state, input = {}) {
|
|
57
|
+
const scoped = scopePayload(input, "locationPinsForScope input");
|
|
58
|
+
return Object.values(state.pins)
|
|
59
|
+
.filter((pin) => !pin.removedAt && pin.scopeType === scoped.scopeType && pin.scopeId === scoped.scopeId)
|
|
60
|
+
.sort((left, right) => right.updatedAt - left.updatedAt);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pinId(payload) {
|
|
64
|
+
return `pin_${scopeKey(payload.scopeType, payload.scopeId).replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const locationPinsHostPlugin = defineHostPlugin({
|
|
68
|
+
id: LOCATION_PINS_PLUGIN_ID,
|
|
69
|
+
version: BASE_PLUGIN_VERSION,
|
|
70
|
+
meta: { name: "Reusable Location Pins" },
|
|
71
|
+
capabilities: { requires: ["room.state", "room.roles"], provides: ["location.pins", "location.coordinates"] },
|
|
72
|
+
stateSchemaDescriptor: { plugin: LOCATION_PINS_PLUGIN_ID, shape: ["pins", "activity"] },
|
|
73
|
+
operationSchemaDescriptor: createOperationSchemaDescriptor(LOCATION_PINS_PLUGIN_ID, BASE_PLUGIN_VERSION, {
|
|
74
|
+
"location.pin.set": {
|
|
75
|
+
required: { scopeType: { type: "string" }, scopeId: { type: "string" }, lat: { type: "number" }, lng: { type: "number" } },
|
|
76
|
+
optional: { zoom: { type: "number" }, label: { type: "string" }, address: { type: "string" } },
|
|
77
|
+
authorize: { roles: ["member"] }
|
|
78
|
+
},
|
|
79
|
+
"location.pin.clear": { required: { scopeType: { type: "string" }, scopeId: { type: "string" } }, authorize: { roles: ["admin"] } }
|
|
80
|
+
}),
|
|
81
|
+
schemas: {
|
|
82
|
+
state: { parse(value) { const state = object(value, "location pins state"); object(state.pins, "pins"); array(state.activity, "activity"); return state; } },
|
|
83
|
+
operations: {
|
|
84
|
+
"location.pin.set": { parse: locationPinSetPayload },
|
|
85
|
+
"location.pin.clear": { parse: locationPinClearPayload }
|
|
86
|
+
},
|
|
87
|
+
publicView: { parse: readonlyState },
|
|
88
|
+
queries: { locationPinsForScope: { parse: readonlyState } }
|
|
89
|
+
},
|
|
90
|
+
async createInitialState() { return { pins: {}, activity: [] }; },
|
|
91
|
+
authorize(ctx, op) {
|
|
92
|
+
if (op.type === "location.pin.set") {
|
|
93
|
+
if (!memberOrBetter(op.actor)) return deny("Members only");
|
|
94
|
+
return ctx.access.canEdit(op.payload.scopeType, op.payload.scopeId) ? allow() : deny("Scoped edit access required");
|
|
95
|
+
}
|
|
96
|
+
if (op.type === "location.pin.clear") return adminOrOwner(op.actor) ? allow() : deny("Admins only");
|
|
97
|
+
return deny("Unsupported location pin operation");
|
|
98
|
+
},
|
|
99
|
+
async reduce(_ctx, state, op) {
|
|
100
|
+
const id = pinId(op.payload);
|
|
101
|
+
if (op.type === "location.pin.clear") {
|
|
102
|
+
const existing = state.pins[id];
|
|
103
|
+
if (!existing || existing.removedAt) return state;
|
|
104
|
+
const nextPin = { ...existing, removedAt: op.createdAt, removedBy: op.actor.memberId };
|
|
105
|
+
return {
|
|
106
|
+
...state,
|
|
107
|
+
pins: { ...state.pins, [id]: nextPin },
|
|
108
|
+
activity: activity(state, op, `${actorName(op.actor)} cleared ${existing.label || "a location pin"}`)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const pin = {
|
|
112
|
+
id,
|
|
113
|
+
scopeType: op.payload.scopeType,
|
|
114
|
+
scopeId: op.payload.scopeId,
|
|
115
|
+
scopeKey: scopeKey(op.payload.scopeType, op.payload.scopeId),
|
|
116
|
+
lat: op.payload.lat,
|
|
117
|
+
lng: op.payload.lng,
|
|
118
|
+
zoom: op.payload.zoom,
|
|
119
|
+
label: op.payload.label,
|
|
120
|
+
address: op.payload.address,
|
|
121
|
+
updatedBy: op.actor.memberId,
|
|
122
|
+
updatedByName: actorName(op.actor),
|
|
123
|
+
updatedAt: op.createdAt
|
|
124
|
+
};
|
|
125
|
+
return {
|
|
126
|
+
...state,
|
|
127
|
+
pins: { ...state.pins, [pin.id]: pin },
|
|
128
|
+
activity: activity(state, op, `${actorName(op.actor)} pinned ${pin.label || pin.scopeId}`)
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
getPublicView(ctx, state) {
|
|
132
|
+
return { ...state, pins: Object.fromEntries(Object.entries(state.pins).filter(([, pin]) => !pin.removedAt && ctx.access.canView(pin.scopeType, pin.scopeId))) };
|
|
133
|
+
},
|
|
134
|
+
queries: { locationPinsForScope }
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
LOCATION_PINS_PLUGIN_ID,
|
|
139
|
+
LOCATION_PINS_PLUGIN_KEY,
|
|
140
|
+
locationPinsHostPlugin,
|
|
141
|
+
normalizePin
|
|
142
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const {
|
|
2
|
+
allow,
|
|
3
|
+
activity,
|
|
4
|
+
actorName,
|
|
5
|
+
array,
|
|
6
|
+
createOperationSchemaDescriptor,
|
|
7
|
+
defineHostPlugin,
|
|
8
|
+
deny,
|
|
9
|
+
entityId,
|
|
10
|
+
enumValue,
|
|
11
|
+
object,
|
|
12
|
+
parseMaybeJsonList,
|
|
13
|
+
readonlyState,
|
|
14
|
+
scopeKey,
|
|
15
|
+
scopePayload,
|
|
16
|
+
string,
|
|
17
|
+
memberOrBetter,
|
|
18
|
+
moderatorOrBetter,
|
|
19
|
+
BASE_PLUGIN_VERSION,
|
|
20
|
+
MARKDOWN_PLUGIN_ID,
|
|
21
|
+
MAX_MARKDOWN_BYTES
|
|
22
|
+
} = require("../../shared/index.cjs");
|
|
23
|
+
const { parseMarkdown } = require("../parser/index.cjs");
|
|
24
|
+
|
|
25
|
+
function parseMarkdownDocumentState(state) {
|
|
26
|
+
const value = object(state, "markdown state");
|
|
27
|
+
object(value.documents, "documents");
|
|
28
|
+
array(value.activity, "activity");
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function markdownUpsertPayload(payload) {
|
|
32
|
+
const value = object(payload, "markdown.document.upsert payload");
|
|
33
|
+
return {
|
|
34
|
+
...scopePayload(value, "markdown scope"),
|
|
35
|
+
title: string(value.title, "title", 180),
|
|
36
|
+
markdown: string(value.markdown, "markdown", MAX_MARKDOWN_BYTES),
|
|
37
|
+
visibility: enumValue(value.visibility || "members", "visibility", ["public", "members", "private"]),
|
|
38
|
+
tags: parseMaybeJsonList(value.tags, "tags")
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function markdownDeletePayload(payload) {
|
|
42
|
+
const value = object(payload, "markdown.document.delete payload");
|
|
43
|
+
return { documentId: string(value.documentId, "documentId") };
|
|
44
|
+
}
|
|
45
|
+
const markdownStateSchemaDescriptor = { plugin: MARKDOWN_PLUGIN_ID, shape: ["documents", "activity"] };
|
|
46
|
+
const markdownOperationSchemaDescriptor = {
|
|
47
|
+
"markdown.document.upsert": { required: ["scopeType", "scopeId", "title", "markdown"], optional: ["visibility", "tags"], authorize: { roles: ["member"] } },
|
|
48
|
+
"markdown.document.delete": { required: ["documentId"], authorize: { roles: ["moderator"] } }
|
|
49
|
+
};
|
|
50
|
+
const markdownHostPlugin = defineHostPlugin({
|
|
51
|
+
id: MARKDOWN_PLUGIN_ID,
|
|
52
|
+
version: BASE_PLUGIN_VERSION,
|
|
53
|
+
meta: { name: "Reusable Markdown Documents" },
|
|
54
|
+
capabilities: { requires: ["room.state", "room.roles"], provides: ["content.markdown", "content.markdown.embeds", "content.sanitized-ast"] },
|
|
55
|
+
stateSchemaDescriptor: markdownStateSchemaDescriptor,
|
|
56
|
+
operationSchemaDescriptor: createOperationSchemaDescriptor(MARKDOWN_PLUGIN_ID, BASE_PLUGIN_VERSION, markdownOperationSchemaDescriptor),
|
|
57
|
+
schemas: {
|
|
58
|
+
state: { parse: parseMarkdownDocumentState },
|
|
59
|
+
operations: { "markdown.document.upsert": { parse: markdownUpsertPayload }, "markdown.document.delete": { parse: markdownDeletePayload } },
|
|
60
|
+
publicView: { parse: readonlyState },
|
|
61
|
+
queries: { documentForScope: { parse: readonlyState }, searchDocuments: { parse: readonlyState } }
|
|
62
|
+
},
|
|
63
|
+
async createInitialState() { return { documents: {}, activity: [] }; },
|
|
64
|
+
authorize(_ctx, op) {
|
|
65
|
+
if (op.type === "markdown.document.upsert") return memberOrBetter(op.actor) ? allow() : deny("Members only");
|
|
66
|
+
if (op.type === "markdown.document.delete") return moderatorOrBetter(op.actor) ? allow() : deny("Moderators only");
|
|
67
|
+
return deny("Unsupported markdown operation");
|
|
68
|
+
},
|
|
69
|
+
async reduce(_ctx, state, op) {
|
|
70
|
+
if (op.type === "markdown.document.upsert") {
|
|
71
|
+
const existing = Object.values(state.documents).find((doc) => doc.scopeType === op.payload.scopeType && doc.scopeId === op.payload.scopeId && !doc.deletedAt);
|
|
72
|
+
const id = existing?.id || entityId("md", op);
|
|
73
|
+
const parsed = parseMarkdown(op.payload.markdown);
|
|
74
|
+
const doc = {
|
|
75
|
+
id,
|
|
76
|
+
scopeType: op.payload.scopeType,
|
|
77
|
+
scopeId: op.payload.scopeId,
|
|
78
|
+
scopeKey: scopeKey(op.payload.scopeType, op.payload.scopeId),
|
|
79
|
+
title: op.payload.title,
|
|
80
|
+
markdown: op.payload.markdown,
|
|
81
|
+
ast: parsed,
|
|
82
|
+
embeds: parsed.embeds,
|
|
83
|
+
text: parsed.text,
|
|
84
|
+
visibility: op.payload.visibility,
|
|
85
|
+
tags: op.payload.tags,
|
|
86
|
+
updatedBy: op.actor.memberId,
|
|
87
|
+
updatedByName: actorName(op.actor),
|
|
88
|
+
updatedAt: op.createdAt,
|
|
89
|
+
createdAt: existing?.createdAt || op.createdAt,
|
|
90
|
+
createdBy: existing?.createdBy || op.actor.memberId
|
|
91
|
+
};
|
|
92
|
+
return { ...state, documents: { ...state.documents, [id]: doc }, activity: activity(state, op, `${actorName(op.actor)} updated ${doc.title}`, { documentId: id }) };
|
|
93
|
+
}
|
|
94
|
+
if (op.type === "markdown.document.delete") {
|
|
95
|
+
const doc = state.documents[op.payload.documentId];
|
|
96
|
+
if (!doc || doc.deletedAt) throw new Error(`Document ${op.payload.documentId} not found`);
|
|
97
|
+
return { ...state, documents: { ...state.documents, [doc.id]: { ...doc, deletedAt: op.createdAt, deletedBy: op.actor.memberId } }, activity: activity(state, op, `${actorName(op.actor)} deleted ${doc.title}`) };
|
|
98
|
+
}
|
|
99
|
+
return state;
|
|
100
|
+
},
|
|
101
|
+
getPublicView(_ctx, state) {
|
|
102
|
+
return { ...state, documents: Object.fromEntries(Object.entries(state.documents).filter(([, doc]) => !doc.deletedAt && doc.visibility !== "private")) };
|
|
103
|
+
},
|
|
104
|
+
queries: {
|
|
105
|
+
documentForScope(_ctx, state, input = {}) {
|
|
106
|
+
const scoped = scopePayload(input, "documentForScope input");
|
|
107
|
+
return Object.values(state.documents).find((doc) => doc.scopeType === scoped.scopeType && doc.scopeId === scoped.scopeId && !doc.deletedAt) || null;
|
|
108
|
+
},
|
|
109
|
+
searchDocuments(_ctx, state, input = {}) {
|
|
110
|
+
const q = String(input.query || "").toLowerCase().trim();
|
|
111
|
+
return Object.values(state.documents)
|
|
112
|
+
.filter((doc) => !doc.deletedAt && (!q || doc.title.toLowerCase().includes(q) || doc.text.toLowerCase().includes(q) || doc.tags.some((tag) => tag.toLowerCase().includes(q))))
|
|
113
|
+
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
114
|
+
.slice(0, 25)
|
|
115
|
+
.map((doc) => ({ id: doc.id, title: doc.title, scopeType: doc.scopeType, scopeId: doc.scopeId, tags: doc.tags, updatedAt: doc.updatedAt, embedCount: doc.embeds.length }));
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
methods: {
|
|
119
|
+
parse(_ctx, input = {}) { return parseMarkdown(string(object(input, "parse input").markdown, "markdown", MAX_MARKDOWN_BYTES)); }
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
MARKDOWN_PLUGIN_ID,
|
|
125
|
+
markdownHostPlugin,
|
|
126
|
+
markdownOperationSchemaDescriptor,
|
|
127
|
+
markdownStateSchemaDescriptor
|
|
128
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
defineMarkdownPlugin: require("../shared/index.cjs").defineMarkdownPlugin,
|
|
3
|
+
createEmbedRecord: require("../shared/index.cjs").createEmbedRecord,
|
|
4
|
+
...require("./providers/index.cjs"),
|
|
5
|
+
...require("./resolve.cjs"),
|
|
6
|
+
...require("./parser/index.cjs"),
|
|
7
|
+
...require("./documents/index.cjs")
|
|
8
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const { MAX_MARKDOWN_BYTES, normalizeUrl } = require("../../shared/index.cjs");
|
|
2
|
+
const { DEFAULT_MARKDOWN_PLUGINS } = require("../providers/index.cjs");
|
|
3
|
+
const { cleanUrlToken, pluginForDirective, resolveEmbed } = require("../resolve.cjs");
|
|
4
|
+
|
|
5
|
+
function tokeniseInline(text, options = {}) {
|
|
6
|
+
const tokens = [];
|
|
7
|
+
const embeds = [];
|
|
8
|
+
const pattern = /\[([^\]\n]+)\]\((https?:\/\/[^\s)]+)\)|(https?:\/\/[^\s<>)]+)/g;
|
|
9
|
+
let cursor = 0;
|
|
10
|
+
for (const match of text.matchAll(pattern)) {
|
|
11
|
+
if (match.index > cursor) tokens.push({ type: "text", value: text.slice(cursor, match.index) });
|
|
12
|
+
const label = match[1] || match[3];
|
|
13
|
+
const url = cleanUrlToken(match[2] || match[3]);
|
|
14
|
+
let embed = null;
|
|
15
|
+
try {
|
|
16
|
+
embed = resolveEmbed(url, { ...options, title: label });
|
|
17
|
+
embeds.push(embed);
|
|
18
|
+
} catch {
|
|
19
|
+
// Links remain valid text links even when embed resolution fails.
|
|
20
|
+
}
|
|
21
|
+
tokens.push({ type: "link", text: label, url: normalizeUrl(url), ...(embed ? { embed } : {}) });
|
|
22
|
+
cursor = match.index + match[0].length;
|
|
23
|
+
}
|
|
24
|
+
if (cursor < text.length) tokens.push({ type: "text", value: text.slice(cursor) });
|
|
25
|
+
return { tokens, embeds };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseMarkdown(markdown, options = {}) {
|
|
29
|
+
if (typeof markdown !== "string") throw new Error("markdown must be a string");
|
|
30
|
+
if (new TextEncoder().encode(markdown).length > (options.maxBytes || MAX_MARKDOWN_BYTES)) throw new Error("markdown is too large");
|
|
31
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
32
|
+
const nodes = [];
|
|
33
|
+
const embeds = [];
|
|
34
|
+
let index = 0;
|
|
35
|
+
function pushInlineNode(type, text, extra = {}) {
|
|
36
|
+
const inline = tokeniseInline(text, options);
|
|
37
|
+
embeds.push(...inline.embeds);
|
|
38
|
+
nodes.push({ type, text, children: inline.tokens, ...extra });
|
|
39
|
+
}
|
|
40
|
+
while (index < lines.length) {
|
|
41
|
+
const line = lines[index];
|
|
42
|
+
if (!line.trim()) { index += 1; continue; }
|
|
43
|
+
const fence = line.match(/^```\s*([A-Za-z0-9_-]+)?\s*$/);
|
|
44
|
+
if (fence) {
|
|
45
|
+
const language = fence[1] || "text";
|
|
46
|
+
index += 1;
|
|
47
|
+
const code = [];
|
|
48
|
+
while (index < lines.length && !/^```\s*$/.test(lines[index])) { code.push(lines[index]); index += 1; }
|
|
49
|
+
if (index < lines.length) index += 1;
|
|
50
|
+
nodes.push({ type: "code", language, value: code.join("\n") });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const directive = line.match(/^::([A-Za-z0-9_-]+)(?:\[([^\]]*)\])?\((https?:\/\/[^\s)]+)\)\s*$/);
|
|
54
|
+
if (directive) {
|
|
55
|
+
const [, provider, title, url] = directive;
|
|
56
|
+
const plugin = pluginForDirective(provider, options.plugins || DEFAULT_MARKDOWN_PLUGINS);
|
|
57
|
+
if (!plugin) throw new Error(`Unknown markdown directive ${provider}`);
|
|
58
|
+
const embed = { ...plugin.toEmbed(normalizeUrl(url), { title: title || undefined, directive: provider }), pluginId: plugin.id };
|
|
59
|
+
embeds.push(embed);
|
|
60
|
+
nodes.push({ type: "embed", provider: embed.provider, title: embed.title, url: embed.url, embed });
|
|
61
|
+
index += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const heading = line.match(/^(#{1,6})\s+(.+)$/);
|
|
65
|
+
if (heading) {
|
|
66
|
+
pushInlineNode("heading", heading[2].trim(), { depth: heading[1].length });
|
|
67
|
+
index += 1;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const quote = line.match(/^>\s?(.*)$/);
|
|
71
|
+
if (quote) {
|
|
72
|
+
const quoteLines = [quote[1]];
|
|
73
|
+
index += 1;
|
|
74
|
+
while (index < lines.length) {
|
|
75
|
+
const next = lines[index].match(/^>\s?(.*)$/);
|
|
76
|
+
if (!next) break;
|
|
77
|
+
quoteLines.push(next[1]);
|
|
78
|
+
index += 1;
|
|
79
|
+
}
|
|
80
|
+
pushInlineNode("blockquote", quoteLines.join("\n"));
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (/^[-*]\s+/.test(line)) {
|
|
84
|
+
const items = [];
|
|
85
|
+
while (index < lines.length && /^[-*]\s+/.test(lines[index])) {
|
|
86
|
+
const text = lines[index].replace(/^[-*]\s+/, "").trim();
|
|
87
|
+
const inline = tokeniseInline(text, options);
|
|
88
|
+
embeds.push(...inline.embeds);
|
|
89
|
+
items.push({ text, children: inline.tokens });
|
|
90
|
+
index += 1;
|
|
91
|
+
}
|
|
92
|
+
nodes.push({ type: "list", ordered: false, items });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const paragraph = [line.trim()];
|
|
96
|
+
index += 1;
|
|
97
|
+
while (index < lines.length && lines[index].trim() && !/^(#{1,6})\s+/.test(lines[index]) && !/^::[A-Za-z0-9_-]+/.test(lines[index]) && !/^[-*]\s+/.test(lines[index]) && !/^>/.test(lines[index]) && !/^```/.test(lines[index])) {
|
|
98
|
+
paragraph.push(lines[index].trim());
|
|
99
|
+
index += 1;
|
|
100
|
+
}
|
|
101
|
+
pushInlineNode("paragraph", paragraph.join(" "));
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
kind: "matterhorn.markdown-document",
|
|
105
|
+
version: 1,
|
|
106
|
+
nodes,
|
|
107
|
+
embeds,
|
|
108
|
+
text: renderMarkdownToPlainText({ nodes }),
|
|
109
|
+
embedCount: embeds.length
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderMarkdownToPlainText(parsed) {
|
|
114
|
+
const nodes = parsed?.nodes || [];
|
|
115
|
+
return nodes.map((node) => {
|
|
116
|
+
if (node.type === "code") return node.value;
|
|
117
|
+
if (node.type === "list") return node.items.map((item) => `- ${item.text}`).join("\n");
|
|
118
|
+
if (node.type === "embed") return `[${node.provider}] ${node.title || node.url}`;
|
|
119
|
+
return node.text || "";
|
|
120
|
+
}).filter(Boolean).join("\n").trim();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
parseMarkdown,
|
|
125
|
+
renderMarkdownToPlainText,
|
|
126
|
+
tokeniseInline
|
|
127
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const {
|
|
2
|
+
cleanId,
|
|
3
|
+
createEmbedRecord,
|
|
4
|
+
defineMarkdownPlugin,
|
|
5
|
+
directRenderMode,
|
|
6
|
+
firstMatch,
|
|
7
|
+
hostEndsWith,
|
|
8
|
+
hostIs,
|
|
9
|
+
mediaKindFromPath,
|
|
10
|
+
normalizeUrl,
|
|
11
|
+
pathParts,
|
|
12
|
+
safeHostname,
|
|
13
|
+
simpleProvider
|
|
14
|
+
} = require("../../shared/index.cjs");
|
|
15
|
+
|
|
16
|
+
const spotifyMarkdownPlugin = defineMarkdownPlugin({
|
|
17
|
+
id: "markdown.embed.spotify",
|
|
18
|
+
provider: "spotify",
|
|
19
|
+
aliases: ["spotify"],
|
|
20
|
+
matchUrl(url) { return safeHostname(url) === "open.spotify.com"; },
|
|
21
|
+
toEmbed(url, ctx = {}) {
|
|
22
|
+
const parsed = new URL(url);
|
|
23
|
+
const parts = parsed.pathname.split("/").filter(Boolean);
|
|
24
|
+
const type = parts[0];
|
|
25
|
+
const id = parts[1];
|
|
26
|
+
const allowed = ["album", "artist", "episode", "playlist", "show", "track"];
|
|
27
|
+
if (!allowed.includes(type) || !id) throw new Error("Spotify URL must point to an album, artist, episode, playlist, show, or track");
|
|
28
|
+
return createEmbedRecord({
|
|
29
|
+
provider: "spotify",
|
|
30
|
+
kind: type === "episode" || type === "show" ? "audio-show" : "audio",
|
|
31
|
+
title: ctx.title || `Spotify ${type}`,
|
|
32
|
+
url,
|
|
33
|
+
embedUrl: `https://open.spotify.com/embed/${type}/${encodeURIComponent(id)}`,
|
|
34
|
+
renderMode: "iframe",
|
|
35
|
+
metadata: { spotifyType: type, spotifyId: id }
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const soundcloudMarkdownPlugin = defineMarkdownPlugin({
|
|
41
|
+
id: "markdown.embed.soundcloud",
|
|
42
|
+
provider: "soundcloud",
|
|
43
|
+
aliases: ["soundcloud"],
|
|
44
|
+
matchUrl(url) { return hostIs(url, ["soundcloud.com"]); },
|
|
45
|
+
toEmbed(url, ctx = {}) { return createEmbedRecord({ provider: "soundcloud", kind: "audio", title: ctx.title || "SoundCloud track", url, embedUrl: `https://w.soundcloud.com/player/?url=${encodeURIComponent(normalizeUrl(url))}`, renderMode: "iframe", metadata: { host: safeHostname(url) } }); }
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const bandcampMarkdownPlugin = simpleProvider({
|
|
49
|
+
id: "markdown.embed.bandcamp",
|
|
50
|
+
provider: "bandcamp",
|
|
51
|
+
aliases: ["bandcamp"],
|
|
52
|
+
hostSuffixes: ["bandcamp.com"],
|
|
53
|
+
kind: "audio",
|
|
54
|
+
title: "Bandcamp item",
|
|
55
|
+
renderMode: "external-link",
|
|
56
|
+
metadata: (parsed) => ({ artistHost: parsed.hostname.toLowerCase() })
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const appleMusicMarkdownPlugin = simpleProvider({
|
|
60
|
+
id: "markdown.embed.apple-music",
|
|
61
|
+
provider: "apple-music",
|
|
62
|
+
aliases: ["apple-music", "music"],
|
|
63
|
+
hosts: ["music.apple.com"],
|
|
64
|
+
kind: "audio",
|
|
65
|
+
title: "Apple Music item",
|
|
66
|
+
renderMode: "external-link"
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const clypMarkdownPlugin = simpleProvider({ id: "markdown.embed.clyp", provider: "clyp", aliases: ["clyp"], hostSuffixes: ["clyp.it"], kind: "audio", title: "Clyp audio", renderMode: "external-link" });
|
|
70
|
+
|
|
71
|
+
module.exports = {
|
|
72
|
+
appleMusicMarkdownPlugin,
|
|
73
|
+
bandcampMarkdownPlugin,
|
|
74
|
+
clypMarkdownPlugin,
|
|
75
|
+
soundcloudMarkdownPlugin,
|
|
76
|
+
spotifyMarkdownPlugin
|
|
77
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const {
|
|
2
|
+
cleanId,
|
|
3
|
+
createEmbedRecord,
|
|
4
|
+
defineMarkdownPlugin,
|
|
5
|
+
directRenderMode,
|
|
6
|
+
firstMatch,
|
|
7
|
+
hostEndsWith,
|
|
8
|
+
hostIs,
|
|
9
|
+
mediaKindFromPath,
|
|
10
|
+
normalizeUrl,
|
|
11
|
+
pathParts,
|
|
12
|
+
safeHostname,
|
|
13
|
+
simpleProvider
|
|
14
|
+
} = require("../../shared/index.cjs");
|
|
15
|
+
|
|
16
|
+
const googleDriveMarkdownPlugin = defineMarkdownPlugin({
|
|
17
|
+
id: "markdown.embed.google-drive",
|
|
18
|
+
provider: "google-drive",
|
|
19
|
+
aliases: ["drive", "gdrive", "google-drive", "docs", "sheets", "slides"],
|
|
20
|
+
matchUrl(url) { const host = safeHostname(url); return host === "drive.google.com" || host === "docs.google.com"; },
|
|
21
|
+
toEmbed(url, ctx = {}) {
|
|
22
|
+
const parsed = new URL(url);
|
|
23
|
+
const host = parsed.hostname.toLowerCase().replace(/^www\./, "");
|
|
24
|
+
const fileId = firstMatch([/\/file\/d\/([^/]+)/, /[?&]id=([^&]+)/], `${parsed.pathname}${parsed.search}`)?.[1];
|
|
25
|
+
const documentId = firstMatch([/\/document\/d\/([^/]+)/, /\/spreadsheets\/d\/([^/]+)/, /\/presentation\/d\/([^/]+)/], parsed.pathname)?.[1];
|
|
26
|
+
const docKind = parsed.pathname.includes("/spreadsheets/") ? "spreadsheet" : parsed.pathname.includes("/presentation/") ? "presentation" : "document";
|
|
27
|
+
const embedUrl = fileId
|
|
28
|
+
? `https://drive.google.com/file/d/${encodeURIComponent(fileId)}/preview`
|
|
29
|
+
: documentId
|
|
30
|
+
? `https://${host}${parsed.pathname.replace(/\/(edit|view).*$/, "/preview")}`
|
|
31
|
+
: undefined;
|
|
32
|
+
return createEmbedRecord({
|
|
33
|
+
provider: "google-drive",
|
|
34
|
+
kind: fileId ? "file" : docKind,
|
|
35
|
+
title: ctx.title || "Google Drive item",
|
|
36
|
+
url,
|
|
37
|
+
...(embedUrl ? { embedUrl } : {}),
|
|
38
|
+
renderMode: embedUrl ? "iframe" : "external-link",
|
|
39
|
+
metadata: { fileId: fileId || null, documentId: documentId || null, docKind }
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const googlePhotosMarkdownPlugin = defineMarkdownPlugin({
|
|
45
|
+
id: "markdown.embed.google-photos",
|
|
46
|
+
provider: "google-photos",
|
|
47
|
+
aliases: ["gphotos", "google-photos", "photos"],
|
|
48
|
+
matchUrl(url) { const host = safeHostname(url); return host === "photos.app.goo.gl" || host === "photos.google.com"; },
|
|
49
|
+
toEmbed(url, ctx = {}) { return createEmbedRecord({ provider: "google-photos", kind: "photo-album", title: ctx.title || "Google Photos album", url, renderMode: "external-link", metadata: { host: safeHostname(url) } }); }
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const dropboxMarkdownPlugin = defineMarkdownPlugin({
|
|
53
|
+
id: "markdown.embed.dropbox",
|
|
54
|
+
provider: "dropbox",
|
|
55
|
+
aliases: ["dropbox", "dbx"],
|
|
56
|
+
matchUrl(url) { const host = safeHostname(url); return host === "dropbox.com" || host === "dl.dropboxusercontent.com"; },
|
|
57
|
+
toEmbed(url, ctx = {}) {
|
|
58
|
+
const parsed = new URL(url);
|
|
59
|
+
parsed.searchParams.delete("dl");
|
|
60
|
+
parsed.searchParams.set("raw", "1");
|
|
61
|
+
return createEmbedRecord({ provider: "dropbox", kind: mediaKindFromPath(parsed.pathname), title: ctx.title || "Dropbox file", url, externalUrl: url, embedUrl: parsed.toString(), renderMode: "file-preview", metadata: { host: safeHostname(url) } });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const onedriveMarkdownPlugin = simpleProvider({ id: "markdown.embed.onedrive", provider: "onedrive", aliases: ["onedrive", "1drv"], hosts: ["onedrive.live.com", "1drv.ms"], kind: "file", title: "OneDrive item", renderMode: "external-link" });
|
|
66
|
+
|
|
67
|
+
module.exports = {
|
|
68
|
+
dropboxMarkdownPlugin,
|
|
69
|
+
googleDriveMarkdownPlugin,
|
|
70
|
+
googlePhotosMarkdownPlugin,
|
|
71
|
+
onedriveMarkdownPlugin
|
|
72
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const {
|
|
2
|
+
cleanId,
|
|
3
|
+
createEmbedRecord,
|
|
4
|
+
defineMarkdownPlugin,
|
|
5
|
+
directRenderMode,
|
|
6
|
+
firstMatch,
|
|
7
|
+
hostEndsWith,
|
|
8
|
+
hostIs,
|
|
9
|
+
mediaKindFromPath,
|
|
10
|
+
normalizeUrl,
|
|
11
|
+
pathParts,
|
|
12
|
+
safeHostname,
|
|
13
|
+
simpleProvider
|
|
14
|
+
} = require("../../shared/index.cjs");
|
|
15
|
+
|
|
16
|
+
const codepenMarkdownPlugin = defineMarkdownPlugin({
|
|
17
|
+
id: "markdown.embed.codepen",
|
|
18
|
+
provider: "codepen",
|
|
19
|
+
aliases: ["codepen", "pen"],
|
|
20
|
+
matchUrl(url) { return hostIs(url, ["codepen.io"]) && pathParts(url)[1] === "pen"; },
|
|
21
|
+
toEmbed(url, ctx = {}) { const parts = pathParts(url); return createEmbedRecord({ provider: "codepen", kind: "code-demo", title: ctx.title || "CodePen", url, embedUrl: `https://codepen.io/${encodeURIComponent(parts[0])}/embed/${encodeURIComponent(parts[2])}`, renderMode: "iframe", metadata: { user: parts[0], penId: parts[2] } }); }
|
|
22
|
+
});
|
|
23
|
+
const jsfiddleMarkdownPlugin = defineMarkdownPlugin({
|
|
24
|
+
id: "markdown.embed.jsfiddle",
|
|
25
|
+
provider: "jsfiddle",
|
|
26
|
+
aliases: ["jsfiddle", "fiddle"],
|
|
27
|
+
matchUrl(url) { return hostIs(url, ["jsfiddle.net"]); },
|
|
28
|
+
toEmbed(url, ctx = {}) { const parsed = new URL(url); const embedPath = parsed.pathname.replace(/\/$/, "") + "/embedded/"; return createEmbedRecord({ provider: "jsfiddle", kind: "code-demo", title: ctx.title || "JSFiddle", url, embedUrl: `https://jsfiddle.net${embedPath}`, renderMode: "iframe", metadata: { path: parsed.pathname } }); }
|
|
29
|
+
});
|
|
30
|
+
const githubMarkdownPlugin = simpleProvider({ id: "markdown.embed.github", provider: "github", aliases: ["github", "gh"], hosts: ["github.com"], kind: (parsed) => parsed.pathname.includes("/pull/") ? "pull-request" : parsed.pathname.includes("/issues/") ? "issue" : parsed.pathname.includes("/blob/") ? "code-file" : "repository", title: "GitHub link", renderMode: "external-link" });
|
|
31
|
+
const gistMarkdownPlugin = simpleProvider({ id: "markdown.embed.gist", provider: "gist", aliases: ["gist"], hosts: ["gist.github.com"], kind: "code-snippet", title: "GitHub Gist", renderMode: "external-link" });
|
|
32
|
+
const pastebinMarkdownPlugin = defineMarkdownPlugin({ id: "markdown.embed.pastebin", provider: "pastebin", aliases: ["pastebin"], matchUrl(url) { return hostIs(url, ["pastebin.com"]) && pathParts(url)[0]; }, toEmbed(url, ctx = {}) { const id = pathParts(url)[0]; return createEmbedRecord({ provider: "pastebin", kind: "code-snippet", title: ctx.title || "Pastebin", url, embedUrl: `https://pastebin.com/embed_iframe/${cleanId(id)}`, renderMode: "iframe", metadata: { pasteId: id } }); } });
|
|
33
|
+
const hastebinMarkdownPlugin = simpleProvider({ id: "markdown.embed.hastebin", provider: "hastebin", aliases: ["hastebin"], hostSuffixes: ["hastebin.com", "hastebin.app"], kind: "code-snippet", title: "Hastebin", renderMode: "external-link" });
|
|
34
|
+
|
|
35
|
+
const wikipediaMarkdownPlugin = simpleProvider({ id: "markdown.embed.wikipedia", provider: "wikipedia", aliases: ["wikipedia", "wiki"], hostSuffixes: ["wikipedia.org", "wikimedia.org"], kind: "article", title: (parsed) => pathParts(parsed.toString()).at(-1)?.replace(/_/g, " ") || "Wikipedia article", renderMode: "external-link" });
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
codepenMarkdownPlugin,
|
|
39
|
+
gistMarkdownPlugin,
|
|
40
|
+
githubMarkdownPlugin,
|
|
41
|
+
hastebinMarkdownPlugin,
|
|
42
|
+
jsfiddleMarkdownPlugin,
|
|
43
|
+
pastebinMarkdownPlugin,
|
|
44
|
+
wikipediaMarkdownPlugin
|
|
45
|
+
};
|