-
Notifications
You must be signed in to change notification settings - Fork 1
/
markdownIt.ts
164 lines (136 loc) · 5.48 KB
/
markdownIt.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
import type MarkdownIt = require("markdown-it");
import type Renderer = require("markdown-it/lib/renderer");
import type Token = require("markdown-it/lib/token");
import localization from "../localization";
declare const webviewApi: any;
// We need to pass [editSvgCommandIdentifier] as an argument because we're converting
// editImage to a string.
const editImage = (contentScriptId: string, container: HTMLElement, svgId: string) => {
const imageElem = container.querySelector('img') ?? document.querySelector(`img#${svgId}`);
if (!imageElem?.src) {
throw new Error(`${imageElem} lacks an src attribute. Unable to edit!`);
}
const updateCachebreaker = (initialSrc: string) => {
// Strip the ?t=... at the end of the image URL
const cachebreakerMatch = /^(.*)\?t=(\d+)$/.exec(initialSrc);
const fileUrl = cachebreakerMatch ? cachebreakerMatch[1] : initialSrc;
const oldCachebreaker = cachebreakerMatch ? parseInt(cachebreakerMatch[2]) : 0;
const newCachebreaker = (new Date()).getTime();
// Add the cachebreaker to the global list -- we may need to change cachebreakers
// on future rerenders.
(window as any)['outOfDateCacheBreakers'] ??= {};
(window as any)['outOfDateCacheBreakers'][fileUrl] = {
outdated: oldCachebreaker,
suggested: newCachebreaker,
};
return `${fileUrl}?t=${newCachebreaker}`;
};
// The webview api is different if we're running in the TinyMce editor vs if we're running
// in the preview pane.
const message = imageElem.src;
const imageElemClass = `imageelem-${(new Date()).getTime()}`;
imageElem.classList.add(imageElemClass);
try {
const postMessage = webviewApi.postMessage;
postMessage(contentScriptId, message).then((resourceId: string|null) => {
// Update all matching
const toRefresh = document.querySelectorAll(`img[data-resource-id="${resourceId}"]`);
for (const elem of toRefresh) {
const imageElem = elem as HTMLImageElement;
imageElem.src = updateCachebreaker(imageElem.src);
}
}).catch((err: any) => {
console.error('Error posting message!', err, '\nMessage: ', message);
});
} catch (err) {
console.warn('Error posting message', err);
console.log('Retrying...');
}
};
const onImgLoad = (container: HTMLElement, buttonId: string) => {
let button = container.querySelector('button.jsdraw--editButton');
const imageElem = container.querySelector('img');
if (!imageElem) {
throw new Error('js-draw editor: Unable to find an image in the given container!');
}
// Another plugin may have moved the button
if (!button) {
button = document.querySelector(`#${buttonId}`);
if (!button) {
throw new Error(`js-draw editor: Unable to find the image editor button with ID ${buttonId}`);
}
button.remove();
container.appendChild(button);
}
container.classList.add('jsdraw--svgWrapper');
const outOfDateCacheBreakers = (window as any)['outOfDateCacheBreakers'] ?? {};
const imageSrcMatch = /^(.*)\?t=(\d+)$/.exec(imageElem.src);
if (!imageSrcMatch) {
throw new Error(`${imageElem?.src} doesn't have a cachebreaker! Unable to update it.`);
}
const fileUrl = imageSrcMatch[1];
const cachebreaker = parseInt(imageSrcMatch[2] ?? '0');
const badCachebreaker = outOfDateCacheBreakers[fileUrl] ?? {};
if (isNaN(cachebreaker) || cachebreaker <= badCachebreaker?.outdated) {
imageElem.src = `${fileUrl}?t=${badCachebreaker.suggested}`;
}
let haveWebviewApi = true;
try {
// Attempt to access .postMessage
// Note: We can't just check window.webviewApi because webviewApi seems not to be
// a property on window.
haveWebviewApi = typeof webviewApi.postMessage === 'function';
} catch (e) {
console.error(e);
haveWebviewApi = false;
}
if (!haveWebviewApi) {
console.log(
'The webview library either doesn\'t exist or lacks a postMessage function. Unable to display an edit button.'
);
button?.remove();
}
};
export default (context: { contentScriptId: string }) => {
return {
plugin: (markdownIt: MarkdownIt, _options: any) => {
const editSvgCommandIdentifier = context.contentScriptId;
let idCounter = 0;
const editImageFnString = editImage.toString().replace(/["]/g, '"');
const onImgLoadFnString = onImgLoad.toString().replace(/["]/g, '"');
// Ref: https://github.com/markdown-it/markdown-it/blob/master/docs/architecture.md#renderer
// and the joplin-drawio plugin
const originalRenderer = markdownIt.renderer.rules.image;
markdownIt.renderer.rules.image = (
tokens: Token[], idx: number, options: MarkdownIt.Options, env: any, self: Renderer
): string => {
const defaultHtml = originalRenderer?.(tokens, idx, options, env, self) ?? '';
const svgUrlExp = /src\s*=\s*['"](file:[/][/])?[^'"]*[.]svg([?]t=\d+)?['"]/i;
if (!svgUrlExp.exec(defaultHtml ?? '')) {
return defaultHtml;
}
const buttonId = `io-github-personalizedrefrigerator-js-draw-edit-button-${idCounter}`;
const svgId = `io-github-personalizedrefrigerator-js-draw-editable-svg-${idCounter}`;
idCounter++;
const htmlWithOnload = defaultHtml.replace('<img ', `<img id="${svgId}" onload="(${onImgLoadFnString})(this.parentElement, '${buttonId}')" `);
return `
<span class='jsdraw--svgWrapper' contentEditable='false'>
${htmlWithOnload}
<button
class='jsdraw--editButton'
onclick="(${editImageFnString})('${editSvgCommandIdentifier}', this.parentElement, '${svgId}')"
id="${buttonId}"
>
${localization.edit} 🖊️
</button>
</span>
`;
};
},
assets: () => {
return [
{ name: 'markdownIt.css' }
]
},
}
}