@isrd-isi-edu/ermrestjs 2.3.0 → 2.4.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.
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isrd-isi-edu/ermrestjs",
|
|
3
3
|
"description": "ERMrest client library in JavaScript",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.4.0",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">= 20.0.0",
|
|
8
|
-
"npm": ">=
|
|
8
|
+
"npm": ">= 7.0.0"
|
|
9
9
|
},
|
|
10
10
|
"main": "dist/ermrest.js",
|
|
11
11
|
"module": "dist/ermrest.min.js",
|
|
@@ -42,11 +42,16 @@
|
|
|
42
42
|
"postgresql",
|
|
43
43
|
"library"
|
|
44
44
|
],
|
|
45
|
+
"overrides": {
|
|
46
|
+
"@microsoft/api-extractor": {
|
|
47
|
+
"typescript": "$typescript"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
45
50
|
"dependencies": {
|
|
46
51
|
"@types/lodash-es": "^4.17.12",
|
|
47
52
|
"@types/markdown-it": "^14.1.2",
|
|
48
53
|
"@types/q": "^1.5.8",
|
|
49
|
-
"axios": "1.13.
|
|
54
|
+
"axios": "1.13.5",
|
|
50
55
|
"handlebars": "4.7.8",
|
|
51
56
|
"lodash-es": "^4.17.23",
|
|
52
57
|
"lz-string": "^1.5.0",
|
|
@@ -455,13 +455,18 @@ export class AssetPseudoColumn extends ReferenceColumn {
|
|
|
455
455
|
*/
|
|
456
456
|
get filePreview(): FilePreviewConfig | null {
|
|
457
457
|
if (this._filePreview === undefined) {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
const settings = isObjectAndKeyExists(currDisplay, 'file_preview') ? currDisplay.file_preview : {};
|
|
461
|
-
if (settings === false) {
|
|
458
|
+
// if the colum has markdown-pattern, don't show the file preview
|
|
459
|
+
if (this.display.sourceMarkdownPattern || this._baseCol.getDisplay(this._context).isMarkdownPattern) {
|
|
462
460
|
this._filePreview = null;
|
|
463
461
|
} else {
|
|
464
|
-
|
|
462
|
+
const disp = this._annotation.display;
|
|
463
|
+
const currDisplay = isObjectAndNotNull(disp) ? _getAnnotationValueByContext(this._context, disp) : null;
|
|
464
|
+
const settings = isObjectAndKeyExists(currDisplay, 'file_preview') ? currDisplay.file_preview : {};
|
|
465
|
+
if (settings === false) {
|
|
466
|
+
this._filePreview = null;
|
|
467
|
+
} else {
|
|
468
|
+
this._filePreview = new FilePreviewConfig(settings);
|
|
469
|
+
}
|
|
465
470
|
}
|
|
466
471
|
}
|
|
467
472
|
return this._filePreview;
|
|
@@ -98,13 +98,18 @@ export default class FilePreviewService {
|
|
|
98
98
|
storedFilename?: string,
|
|
99
99
|
contentDisposition?: string,
|
|
100
100
|
contentType?: string,
|
|
101
|
+
forcedPreviewType?: FilePreviewTypes,
|
|
102
|
+
forcedPrefetchBytes?: number,
|
|
103
|
+
forcedPrefetchMaxFileSize?: number,
|
|
101
104
|
): {
|
|
102
105
|
previewType: FilePreviewTypes | null;
|
|
103
106
|
prefetchBytes: number | null;
|
|
104
107
|
prefetchMaxFileSize: number | null;
|
|
108
|
+
filename: string;
|
|
105
109
|
} {
|
|
106
|
-
const disabledValue = { previewType: null, prefetchBytes: null, prefetchMaxFileSize: null };
|
|
107
|
-
const
|
|
110
|
+
const disabledValue = { previewType: null, prefetchBytes: null, prefetchMaxFileSize: null, filename: '' };
|
|
111
|
+
const filename = storedFilename || getFilename(url, contentDisposition);
|
|
112
|
+
const previewType = forcedPreviewType ? forcedPreviewType : FilePreviewService.getFilePreviewType(url, filename, column, contentType);
|
|
108
113
|
let prefetchBytes: number | null = null;
|
|
109
114
|
let prefetchMaxFileSize: number | null = null;
|
|
110
115
|
|
|
@@ -127,30 +132,31 @@ export default class FilePreviewService {
|
|
|
127
132
|
prefetchMaxFileSize = FILE_PREVIEW.MAX_FILE_SIZE;
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
// the forced (manual) setting cannot exceed the column/default settings
|
|
136
|
+
if (typeof forcedPrefetchBytes === 'number' && forcedPrefetchBytes >= 0 && forcedPrefetchBytes < prefetchBytes) {
|
|
137
|
+
prefetchBytes = forcedPrefetchBytes;
|
|
138
|
+
}
|
|
139
|
+
if (typeof forcedPrefetchMaxFileSize === 'number' && forcedPrefetchMaxFileSize >= 0 && forcedPrefetchMaxFileSize < prefetchMaxFileSize) {
|
|
140
|
+
prefetchMaxFileSize = forcedPrefetchMaxFileSize;
|
|
141
|
+
}
|
|
142
|
+
|
|
130
143
|
// if prefetchMaxFileSize is 0, we should not show the preview
|
|
131
144
|
if (prefetchMaxFileSize === 0) {
|
|
132
145
|
return disabledValue;
|
|
133
146
|
}
|
|
134
147
|
|
|
135
|
-
return { previewType, prefetchBytes, prefetchMaxFileSize };
|
|
148
|
+
return { previewType, prefetchBytes, prefetchMaxFileSize, filename };
|
|
136
149
|
}
|
|
137
150
|
|
|
138
151
|
/**
|
|
139
152
|
* Returns the preview type based on the given file properties and the column's file preview settings.
|
|
140
153
|
* @param url the file url
|
|
141
154
|
* @param column the asset column
|
|
142
|
-
* @param
|
|
155
|
+
* @param filename the filename (either stored filename or from content-disposition or url)
|
|
143
156
|
* @param contentDisposition content-disposition header value
|
|
144
157
|
* @param contentType content-type header value
|
|
145
158
|
*/
|
|
146
|
-
private static getFilePreviewType(
|
|
147
|
-
url: string,
|
|
148
|
-
column?: AssetPseudoColumn,
|
|
149
|
-
storedFilename?: string,
|
|
150
|
-
contentDisposition?: string,
|
|
151
|
-
contentType?: string,
|
|
152
|
-
): FilePreviewTypes | null {
|
|
153
|
-
const filename = storedFilename || getFilename(url, contentDisposition);
|
|
159
|
+
private static getFilePreviewType(url: string, filename: string, column?: AssetPseudoColumn, contentType?: string): FilePreviewTypes | null {
|
|
154
160
|
const extension = getFilenameExtension(filename, column?.filenameExtFilter, column?.filenameExtRegexp);
|
|
155
161
|
let mappedFilePreviewType: FilePreviewTypes | typeof USE_EXT_MAPPING | false = USE_EXT_MAPPING;
|
|
156
162
|
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import type MarkdownIt from 'markdown-it';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Custom div block plugin for markdown-it
|
|
5
|
+
* Supports:
|
|
6
|
+
* - Multi-line syntax with attributes: :::div {.class #id attr="value"}\nContent\n:::
|
|
7
|
+
* - Nesting with colon counting: :::, ::::, :::::, etc.
|
|
8
|
+
* - Single-line syntax: :::div content {.class}\n:::
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const MARKER_CHAR = 0x3a; // ':'
|
|
12
|
+
const MIN_MARKERS = 3;
|
|
13
|
+
|
|
14
|
+
// Store markdown-it instance for use in renderers
|
|
15
|
+
let mdInstance: any = null;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Unwrap paragraph tags that contain only a single element, but only within div tags
|
|
19
|
+
* This handles cases like <div><p><img></p></div> -> <div><img></div>
|
|
20
|
+
*/
|
|
21
|
+
function unwrapSingleElementParagraphsInDivs(html: string): string {
|
|
22
|
+
// Pattern to match <div...>...</div> including nested divs
|
|
23
|
+
const divPattern = /<div([^>]*)>([\s\S]*?)<\/div>/g;
|
|
24
|
+
|
|
25
|
+
return html.replace(divPattern, (match, divAttrs, divContent) => {
|
|
26
|
+
// Apply paragraph unwrapping only to content inside this div
|
|
27
|
+
const unwrappedContent = unwrapParagraphsInContent(divContent);
|
|
28
|
+
return `<div${divAttrs}>${unwrappedContent}</div>`;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Unwrap paragraph tags containing single elements from the given content
|
|
34
|
+
*/
|
|
35
|
+
function unwrapParagraphsInContent(content: string): string {
|
|
36
|
+
const pTagPattern = /<p([^>]*)>(.*?)<\/p>/gs;
|
|
37
|
+
|
|
38
|
+
return content.replace(pTagPattern, (match, _pAttrs, pContent) => {
|
|
39
|
+
const trimmedContent = pContent.trim();
|
|
40
|
+
|
|
41
|
+
// Check if content contains only a single HTML element (no surrounding text)
|
|
42
|
+
const singleElementPattern = /^<([a-z][a-z0-9]*)\b[^>]*>.*?<\/\1>$|^<[a-z][a-z0-9]*\b[^>]*\/?>$/is;
|
|
43
|
+
|
|
44
|
+
if (singleElementPattern.test(trimmedContent)) {
|
|
45
|
+
const tagMatch = trimmedContent.match(/^<([a-z][a-z0-9]*)/i);
|
|
46
|
+
if (tagMatch) {
|
|
47
|
+
const tagName = tagMatch[1].toLowerCase();
|
|
48
|
+
// Block-level elements and elements that shouldn't be wrapped in <p>
|
|
49
|
+
const unwrapTags = ['img', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'table', 'figure', 'iframe', 'video'];
|
|
50
|
+
|
|
51
|
+
if (unwrapTags.includes(tagName)) {
|
|
52
|
+
return trimmedContent;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// For inline elements like <a>, only unwrap if it's the ONLY content
|
|
56
|
+
if (tagName === 'a' || tagName === 'span') {
|
|
57
|
+
return trimmedContent;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Keep the <p> tag
|
|
63
|
+
return match;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse the div block opening line to extract attributes
|
|
69
|
+
*/
|
|
70
|
+
function parseOpeningLine(line: string): { attrs: string; hasContent: boolean; content: string } {
|
|
71
|
+
// Match: :::div or :::div {attrs} or :::div content {attrs}
|
|
72
|
+
const match = line.trim().match(/^:+div\s*(.*)$/i);
|
|
73
|
+
if (!match) {
|
|
74
|
+
return { attrs: '', hasContent: false, content: '' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const rest = match[1].trim();
|
|
78
|
+
if (!rest) {
|
|
79
|
+
return { attrs: '', hasContent: false, content: '' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if it's attributes-only pattern: {.class}, {#id}, {attr=value}, or combinations
|
|
83
|
+
const isAttributesOnly = /^(\{[^}]*\}\s*)+$/.test(rest);
|
|
84
|
+
|
|
85
|
+
if (isAttributesOnly) {
|
|
86
|
+
// Multi-line syntax with attributes
|
|
87
|
+
return { attrs: rest, hasContent: false, content: '' };
|
|
88
|
+
} else {
|
|
89
|
+
// Single-line syntax with content (and possibly attributes at the end)
|
|
90
|
+
return { attrs: '', hasContent: true, content: rest };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Block rule for div containers
|
|
96
|
+
*/
|
|
97
|
+
function divBlock(state: any, startLine: number, endLine: number, silent: boolean): boolean {
|
|
98
|
+
let pos = state.bMarks[startLine] + state.tShift[startLine];
|
|
99
|
+
let max = state.eMarks[startLine];
|
|
100
|
+
|
|
101
|
+
// Check for block quote or other indentation
|
|
102
|
+
if (state.sCount[startLine] - state.blkIndent >= 4) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Check if line starts with enough colons
|
|
107
|
+
let markerCount = 0;
|
|
108
|
+
while (pos < max && state.src.charCodeAt(pos) === MARKER_CHAR) {
|
|
109
|
+
markerCount++;
|
|
110
|
+
pos++;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (markerCount < MIN_MARKERS) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Must be followed by 'div' (case insensitive)
|
|
118
|
+
const restOfLine = state.src.slice(pos, max);
|
|
119
|
+
if (!restOfLine.match(/^div(\s|$)/i)) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// If in validation mode, we're done
|
|
124
|
+
if (silent) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Parse the opening line
|
|
129
|
+
const fullLine = state.src.slice(state.bMarks[startLine], max);
|
|
130
|
+
const { attrs, hasContent, content } = parseOpeningLine(fullLine);
|
|
131
|
+
|
|
132
|
+
// Find the closing marker
|
|
133
|
+
let nextLine = startLine;
|
|
134
|
+
let autoClose = false;
|
|
135
|
+
|
|
136
|
+
for (;;) {
|
|
137
|
+
nextLine++;
|
|
138
|
+
if (nextLine >= endLine) {
|
|
139
|
+
// Reached end without finding closing marker
|
|
140
|
+
autoClose = true;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
pos = state.bMarks[nextLine] + state.tShift[nextLine];
|
|
145
|
+
max = state.eMarks[nextLine];
|
|
146
|
+
|
|
147
|
+
if (pos < max && state.sCount[nextLine] < state.blkIndent) {
|
|
148
|
+
// Non-empty line with negative indent should stop the block
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for closing marker with same number of colons
|
|
153
|
+
let closeMarkerCount = 0;
|
|
154
|
+
let checkPos = pos;
|
|
155
|
+
while (checkPos < max && state.src.charCodeAt(checkPos) === MARKER_CHAR) {
|
|
156
|
+
closeMarkerCount++;
|
|
157
|
+
checkPos++;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Closing marker must have exact same number of colons and nothing else (or whitespace)
|
|
161
|
+
if (closeMarkerCount === markerCount) {
|
|
162
|
+
const afterMarker = state.src.slice(checkPos, max).trim();
|
|
163
|
+
if (afterMarker === '' || afterMarker === 'div') {
|
|
164
|
+
// Found closing marker
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const oldParent = state.parentType;
|
|
171
|
+
const oldLineMax = state.lineMax;
|
|
172
|
+
state.parentType = 'container';
|
|
173
|
+
|
|
174
|
+
// Create opening token
|
|
175
|
+
let token: any = state.push('div_open', 'div', 1);
|
|
176
|
+
token.markup = ':'.repeat(markerCount);
|
|
177
|
+
token.block = true;
|
|
178
|
+
token.meta = { attrs, hasContent, content }; // Store in meta instead of info
|
|
179
|
+
token.map = [startLine, nextLine];
|
|
180
|
+
|
|
181
|
+
if (!hasContent) {
|
|
182
|
+
// Multi-line syntax: parse content between markers
|
|
183
|
+
const contentStart = startLine + 1;
|
|
184
|
+
const contentEnd = nextLine;
|
|
185
|
+
|
|
186
|
+
if (contentStart < contentEnd) {
|
|
187
|
+
state.lineMax = contentEnd;
|
|
188
|
+
state.md.block.tokenize(state, contentStart, contentEnd);
|
|
189
|
+
state.lineMax = oldLineMax;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// For single-line syntax (hasContent === true), we don't create any tokens
|
|
193
|
+
// The content will be rendered directly in the div_open renderer
|
|
194
|
+
|
|
195
|
+
// Create closing token
|
|
196
|
+
token = state.push('div_close', 'div', -1);
|
|
197
|
+
token.markup = ':'.repeat(markerCount);
|
|
198
|
+
token.block = true;
|
|
199
|
+
|
|
200
|
+
state.parentType = oldParent;
|
|
201
|
+
state.line = nextLine + (autoClose ? 0 : 1);
|
|
202
|
+
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Renderer for div_open token
|
|
208
|
+
*/
|
|
209
|
+
function renderDivOpen(tokens: any[], idx: number): string {
|
|
210
|
+
const token = tokens[idx];
|
|
211
|
+
|
|
212
|
+
// Get metadata from token
|
|
213
|
+
const meta = token.meta || {};
|
|
214
|
+
const { attrs = '', hasContent = false, content = '' } = meta;
|
|
215
|
+
|
|
216
|
+
if (hasContent && content) {
|
|
217
|
+
// Single-line syntax: render the content and extract attributes from the paragraph tag
|
|
218
|
+
const html = mdInstance.render(content).trim();
|
|
219
|
+
|
|
220
|
+
// html should be like '<p class="class">content</p>' or just '<p>content</p>'
|
|
221
|
+
if (html.startsWith('<p') && html.endsWith('</p>')) {
|
|
222
|
+
// Extract attributes from <p> tag and content
|
|
223
|
+
const match = html.match(/^<p([^>]*)>(.*)<\/p>$/);
|
|
224
|
+
if (match) {
|
|
225
|
+
const extractedAttrs = match[1]; // attributes like ' class="class"'
|
|
226
|
+
const extractedContent = match[2]; // the actual content
|
|
227
|
+
|
|
228
|
+
// Return the div with attributes and content, and DON'T close it yet
|
|
229
|
+
// (div_close will handle closing)
|
|
230
|
+
return `<div${extractedAttrs}>${extractedContent}`;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Fallback: just render the content normally
|
|
235
|
+
return '<div>' + html.replace(/^<p[^>]*>/, '').replace(/<\/p>$/, '');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Multi-line syntax: check attrs for attributes
|
|
239
|
+
if (attrs) {
|
|
240
|
+
// Render a dummy content with the attributes to extract them
|
|
241
|
+
const tempContent = 'x ' + attrs;
|
|
242
|
+
const html = mdInstance.render(tempContent).trim();
|
|
243
|
+
|
|
244
|
+
// html should be something like '<p class="class">x</p>' or '<p class="class" id="id">x</p>'
|
|
245
|
+
if (html.startsWith('<p') && html.endsWith('</p>')) {
|
|
246
|
+
const match = html.match(/^<p([^>]*)>.*<\/p>$/);
|
|
247
|
+
if (match && match[1]) {
|
|
248
|
+
return `<div${match[1]}>`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return '<div>';
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Renderer for div_close token
|
|
258
|
+
*/
|
|
259
|
+
function renderDivClose(): string {
|
|
260
|
+
return '</div>\n';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Plugin registration function
|
|
265
|
+
*/
|
|
266
|
+
export function markdownDivBlock(md: MarkdownIt): void {
|
|
267
|
+
// Store the markdown-it instance for use in renderers
|
|
268
|
+
mdInstance = md;
|
|
269
|
+
|
|
270
|
+
// Register the block rule before fence
|
|
271
|
+
md.block.ruler.before('fence', 'div_block', divBlock, {
|
|
272
|
+
alt: ['paragraph', 'reference', 'blockquote', 'list'],
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Register renderers
|
|
276
|
+
md.renderer.rules.div_open = renderDivOpen;
|
|
277
|
+
md.renderer.rules.div_close = renderDivClose;
|
|
278
|
+
|
|
279
|
+
// Post-process rendered HTML to unwrap single-element paragraphs ONLY in divs
|
|
280
|
+
const originalRender = md.render.bind(md);
|
|
281
|
+
md.render = function (src: string, env?: any) {
|
|
282
|
+
const html = originalRender(src, env);
|
|
283
|
+
return unwrapSingleElementParagraphsInDivs(html);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const originalRenderInline = md.renderInline.bind(md);
|
|
287
|
+
md.renderInline = function (src: string, env?: any) {
|
|
288
|
+
const html = originalRenderInline(src, env);
|
|
289
|
+
return unwrapSingleElementParagraphsInDivs(html);
|
|
290
|
+
};
|
|
291
|
+
}
|
|
@@ -5,6 +5,7 @@ import $log from '@isrd-isi-edu/ermrestjs/src/services/logger';
|
|
|
5
5
|
|
|
6
6
|
// utils
|
|
7
7
|
import { _classNames } from '@isrd-isi-edu/ermrestjs/src/utils/constants';
|
|
8
|
+
import { markdownDivBlock } from '@isrd-isi-edu/ermrestjs/src/utils/markdown-div-block';
|
|
8
9
|
|
|
9
10
|
// vendor
|
|
10
11
|
import markdownItSub from '@isrd-isi-edu/ermrestjs/vendor/markdown-it-sub.min';
|
|
@@ -657,38 +658,8 @@ function _bindCustomMarkdownTags(md: typeof MarkdownIt) {
|
|
|
657
658
|
},
|
|
658
659
|
});
|
|
659
660
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
* Checks whether string matches format ":::div CONTENT \n:::"
|
|
663
|
-
* string inside `{}` is optional, specifies attributes to be applied to element
|
|
664
|
-
*/
|
|
665
|
-
validate: function (params: any) {
|
|
666
|
-
return params.trim().match(/^div\s+(.*)$/i);
|
|
667
|
-
},
|
|
668
|
-
|
|
669
|
-
render: function (tokens: any, idx: any) {
|
|
670
|
-
const m = tokens[idx].info.trim().match(/^div\s+(.*)$/i);
|
|
671
|
-
|
|
672
|
-
// opening tag
|
|
673
|
-
if (tokens[idx].nesting === 1) {
|
|
674
|
-
// if the next tag is a paragraph, we can change the paragraph into a div
|
|
675
|
-
const attrs = md.parse(m[1], {});
|
|
676
|
-
if (attrs && attrs.length > 0 && attrs[0].type === 'paragraph_open') {
|
|
677
|
-
const html = md.render(m[1]).trim();
|
|
678
|
-
|
|
679
|
-
// this will remove the closing and opening p tag.
|
|
680
|
-
return '<div' + html.slice(2, html.length - 4);
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
// otherwise just add the div tag
|
|
684
|
-
return '<div>\n' + md.render(m[1]).trim();
|
|
685
|
-
}
|
|
686
|
-
// the closing tag
|
|
687
|
-
else {
|
|
688
|
-
return '</div>\n';
|
|
689
|
-
}
|
|
690
|
-
},
|
|
691
|
-
});
|
|
661
|
+
// Use custom div block implementation
|
|
662
|
+
md.use(markdownDivBlock);
|
|
692
663
|
|
|
693
664
|
md.use(MarkdownItContainer, 'geneSequence', {
|
|
694
665
|
validate: function (params: any) {
|
|
@@ -746,6 +717,116 @@ function _bindCustomMarkdownTags(md: typeof MarkdownIt) {
|
|
|
746
717
|
},
|
|
747
718
|
});
|
|
748
719
|
|
|
720
|
+
// Dependent on 'markdown-it-container' and 'markdown-it-attrs' plugins
|
|
721
|
+
// Injects `filePreview` tag that will be rendered as a React component in chaise
|
|
722
|
+
md.use(MarkdownItContainer, 'filePreview', {
|
|
723
|
+
/*
|
|
724
|
+
* Checks whether string matches format "::: filePreview [](URL){ATTR=VALUE .CLASSNAME}"
|
|
725
|
+
* String inside '{}' is Optional, specifies attributes to be applied to the element
|
|
726
|
+
*/
|
|
727
|
+
validate: function (params: any) {
|
|
728
|
+
return params.trim().match(/^filePreview\s+(.*)$/i);
|
|
729
|
+
},
|
|
730
|
+
|
|
731
|
+
render: function (tokens: any, idx: any) {
|
|
732
|
+
// Get token string after regexp matching
|
|
733
|
+
const m = tokens[idx].info.trim().match(/^filePreview\s+(.*)$/i);
|
|
734
|
+
|
|
735
|
+
// If this is the opening tag i.e. starts with "::: filePreview "
|
|
736
|
+
if (tokens[idx].nesting === 1 && m && m.length > 0) {
|
|
737
|
+
// Extract remaining string and get its parsed markdown attributes
|
|
738
|
+
const attrs = md.parseInline(m[1], {});
|
|
739
|
+
let html = '';
|
|
740
|
+
|
|
741
|
+
if (attrs && attrs.length == 1 && attrs[0].children) {
|
|
742
|
+
// Check if the markdown is a link
|
|
743
|
+
if (attrs[0].children[0].type == 'link_open') {
|
|
744
|
+
const openingLink = attrs[0].children[0];
|
|
745
|
+
let fileUrl = '';
|
|
746
|
+
let filename = '';
|
|
747
|
+
let classAttr = '';
|
|
748
|
+
let otherAttrs = '';
|
|
749
|
+
let hideDownloadBtn = false;
|
|
750
|
+
let downloadBtnClass = '';
|
|
751
|
+
let previewType = '';
|
|
752
|
+
let prefetchBytes = '';
|
|
753
|
+
let prefetchMaxFileSize = '';
|
|
754
|
+
|
|
755
|
+
// Extract attributes
|
|
756
|
+
openingLink!.attrs!.forEach(function (attr) {
|
|
757
|
+
switch (attr[0]) {
|
|
758
|
+
case 'href':
|
|
759
|
+
fileUrl = attr[1];
|
|
760
|
+
break;
|
|
761
|
+
case 'filename':
|
|
762
|
+
filename = attr[1];
|
|
763
|
+
break;
|
|
764
|
+
case 'class':
|
|
765
|
+
classAttr = attr[1];
|
|
766
|
+
break;
|
|
767
|
+
case 'preview-type':
|
|
768
|
+
previewType = attr[1];
|
|
769
|
+
break;
|
|
770
|
+
case 'prefetch-bytes':
|
|
771
|
+
prefetchBytes = attr[1];
|
|
772
|
+
break;
|
|
773
|
+
case 'prefetch-max-file-size':
|
|
774
|
+
prefetchMaxFileSize = attr[1];
|
|
775
|
+
break;
|
|
776
|
+
case 'hide-download-btn':
|
|
777
|
+
hideDownloadBtn = true;
|
|
778
|
+
break;
|
|
779
|
+
case 'download-btn-class':
|
|
780
|
+
downloadBtnClass = attr[1];
|
|
781
|
+
break;
|
|
782
|
+
default:
|
|
783
|
+
otherAttrs += ' ' + attr[0] + '="' + attr[1] + '"';
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
let caption = '';
|
|
789
|
+
|
|
790
|
+
// Extract caption as plain text (no HTML parsing)
|
|
791
|
+
if (attrs[0].children[1].type != 'link_close') {
|
|
792
|
+
for (let i = 1; i < attrs[0].children.length; i++) {
|
|
793
|
+
if (attrs[0].children[i].type == 'text') {
|
|
794
|
+
caption += attrs[0].children[i].content;
|
|
795
|
+
} else if (attrs[0].children[i].type !== 'link_close') {
|
|
796
|
+
// Skip non-text tokens (caption must be plain text only)
|
|
797
|
+
continue;
|
|
798
|
+
} else {
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const props = [
|
|
805
|
+
`class="file-preview-placeholder${classAttr ? ' ' + classAttr : ''}"`,
|
|
806
|
+
'data-chaise-file-preview="true"',
|
|
807
|
+
'data-file-url="' + fileUrl + '"',
|
|
808
|
+
filename ? 'data-filename="' + filename + '"' : '',
|
|
809
|
+
otherAttrs,
|
|
810
|
+
previewType ? 'data-preview-type="' + previewType + '"' : '',
|
|
811
|
+
hideDownloadBtn ? 'data-hide-download-btn="true"' : '',
|
|
812
|
+
downloadBtnClass ? 'data-download-btn-class="' + downloadBtnClass + '"' : '',
|
|
813
|
+
caption ? 'data-download-btn-caption="' + caption + '"' : '',
|
|
814
|
+
prefetchBytes ? 'data-prefetch-bytes="' + prefetchBytes + '"' : '',
|
|
815
|
+
prefetchMaxFileSize ? 'data-prefetch-max-file-size="' + prefetchMaxFileSize + '"' : '',
|
|
816
|
+
].filter(Boolean);
|
|
817
|
+
|
|
818
|
+
html = `<div ${props.join(' ')}></div>`;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
return html;
|
|
823
|
+
} else {
|
|
824
|
+
// closing tag
|
|
825
|
+
return '';
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
});
|
|
829
|
+
|
|
749
830
|
// Note: Following how this was done in markdown-it-sub and markdown-it-span
|
|
750
831
|
md.use(function rid_plugin(md) {
|
|
751
832
|
// same as UNESCAPE_MD_RE plus a space
|