@isrd-isi-edu/ermrestjs 2.3.0 → 2.4.1
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,15 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@isrd-isi-edu/ermrestjs",
|
|
3
3
|
"description": "ERMrest client library in JavaScript",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.4.1",
|
|
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",
|
|
12
|
-
"types": "dist/ermrest.d.ts",
|
|
13
12
|
"files": [
|
|
14
13
|
"dist",
|
|
15
14
|
"js",
|
|
@@ -42,11 +41,16 @@
|
|
|
42
41
|
"postgresql",
|
|
43
42
|
"library"
|
|
44
43
|
],
|
|
44
|
+
"overrides": {
|
|
45
|
+
"@microsoft/api-extractor": {
|
|
46
|
+
"typescript": "$typescript"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
45
49
|
"dependencies": {
|
|
46
50
|
"@types/lodash-es": "^4.17.12",
|
|
47
51
|
"@types/markdown-it": "^14.1.2",
|
|
48
52
|
"@types/q": "^1.5.8",
|
|
49
|
-
"axios": "1.13.
|
|
53
|
+
"axios": "1.13.5",
|
|
50
54
|
"handlebars": "4.7.8",
|
|
51
55
|
"lodash-es": "^4.17.23",
|
|
52
56
|
"lz-string": "^1.5.0",
|
|
@@ -59,8 +63,7 @@
|
|
|
59
63
|
"terser": "^5.44.1",
|
|
60
64
|
"typescript": "~5.9.3",
|
|
61
65
|
"vite": "^7.3.1",
|
|
62
|
-
"vite-plugin-compression2": "^2.2.1"
|
|
63
|
-
"vite-plugin-dts": "^4.5.4"
|
|
66
|
+
"vite-plugin-compression2": "^2.2.1"
|
|
64
67
|
},
|
|
65
68
|
"devDependencies": {
|
|
66
69
|
"@commitlint/cli": "^20.2.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;
|
|
@@ -49,6 +49,10 @@ const DEFAULT_CONTENT_TYPE_MAPPING: { [key: string]: FilePreviewTypes | typeof U
|
|
|
49
49
|
// text:
|
|
50
50
|
'chemical/x-mmcif': FilePreviewTypes.TEXT,
|
|
51
51
|
'chemical/x-cif': FilePreviewTypes.TEXT,
|
|
52
|
+
'text/yaml': FilePreviewTypes.TEXT,
|
|
53
|
+
'text/x-yaml': FilePreviewTypes.TEXT,
|
|
54
|
+
'application/yaml': FilePreviewTypes.TEXT,
|
|
55
|
+
'application/x-yaml': FilePreviewTypes.TEXT,
|
|
52
56
|
// generic:
|
|
53
57
|
'text/plain': USE_EXT_MAPPING,
|
|
54
58
|
'application/octet-stream': USE_EXT_MAPPING,
|
|
@@ -78,6 +82,8 @@ const DEFAULT_EXTENSION_MAPPING: { [key: string]: FilePreviewTypes | false } = {
|
|
|
78
82
|
'.mvsj': FilePreviewTypes.JSON, // MolViewSpec JSON (mol* viewer)
|
|
79
83
|
// text:
|
|
80
84
|
'.txt': FilePreviewTypes.TEXT,
|
|
85
|
+
'.yml': FilePreviewTypes.TEXT,
|
|
86
|
+
'.yaml': FilePreviewTypes.TEXT,
|
|
81
87
|
'.log': FilePreviewTypes.TEXT,
|
|
82
88
|
'.cif': FilePreviewTypes.TEXT,
|
|
83
89
|
'.pdb': FilePreviewTypes.TEXT,
|
|
@@ -98,13 +104,18 @@ export default class FilePreviewService {
|
|
|
98
104
|
storedFilename?: string,
|
|
99
105
|
contentDisposition?: string,
|
|
100
106
|
contentType?: string,
|
|
107
|
+
forcedPreviewType?: FilePreviewTypes,
|
|
108
|
+
forcedPrefetchBytes?: number,
|
|
109
|
+
forcedPrefetchMaxFileSize?: number,
|
|
101
110
|
): {
|
|
102
111
|
previewType: FilePreviewTypes | null;
|
|
103
112
|
prefetchBytes: number | null;
|
|
104
113
|
prefetchMaxFileSize: number | null;
|
|
114
|
+
filename: string;
|
|
105
115
|
} {
|
|
106
|
-
const disabledValue = { previewType: null, prefetchBytes: null, prefetchMaxFileSize: null };
|
|
107
|
-
const
|
|
116
|
+
const disabledValue = { previewType: null, prefetchBytes: null, prefetchMaxFileSize: null, filename: '' };
|
|
117
|
+
const filename = storedFilename || getFilename(url, contentDisposition);
|
|
118
|
+
const previewType = forcedPreviewType ? forcedPreviewType : FilePreviewService.getFilePreviewType(url, filename, column, contentType);
|
|
108
119
|
let prefetchBytes: number | null = null;
|
|
109
120
|
let prefetchMaxFileSize: number | null = null;
|
|
110
121
|
|
|
@@ -127,30 +138,31 @@ export default class FilePreviewService {
|
|
|
127
138
|
prefetchMaxFileSize = FILE_PREVIEW.MAX_FILE_SIZE;
|
|
128
139
|
}
|
|
129
140
|
|
|
141
|
+
// the forced (manual) setting cannot exceed the column/default settings
|
|
142
|
+
if (typeof forcedPrefetchBytes === 'number' && forcedPrefetchBytes >= 0 && forcedPrefetchBytes < prefetchBytes) {
|
|
143
|
+
prefetchBytes = forcedPrefetchBytes;
|
|
144
|
+
}
|
|
145
|
+
if (typeof forcedPrefetchMaxFileSize === 'number' && forcedPrefetchMaxFileSize >= 0 && forcedPrefetchMaxFileSize < prefetchMaxFileSize) {
|
|
146
|
+
prefetchMaxFileSize = forcedPrefetchMaxFileSize;
|
|
147
|
+
}
|
|
148
|
+
|
|
130
149
|
// if prefetchMaxFileSize is 0, we should not show the preview
|
|
131
150
|
if (prefetchMaxFileSize === 0) {
|
|
132
151
|
return disabledValue;
|
|
133
152
|
}
|
|
134
153
|
|
|
135
|
-
return { previewType, prefetchBytes, prefetchMaxFileSize };
|
|
154
|
+
return { previewType, prefetchBytes, prefetchMaxFileSize, filename };
|
|
136
155
|
}
|
|
137
156
|
|
|
138
157
|
/**
|
|
139
158
|
* Returns the preview type based on the given file properties and the column's file preview settings.
|
|
140
159
|
* @param url the file url
|
|
141
160
|
* @param column the asset column
|
|
142
|
-
* @param
|
|
161
|
+
* @param filename the filename (either stored filename or from content-disposition or url)
|
|
143
162
|
* @param contentDisposition content-disposition header value
|
|
144
163
|
* @param contentType content-type header value
|
|
145
164
|
*/
|
|
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);
|
|
165
|
+
private static getFilePreviewType(url: string, filename: string, column?: AssetPseudoColumn, contentType?: string): FilePreviewTypes | null {
|
|
154
166
|
const extension = getFilenameExtension(filename, column?.filenameExtFilter, column?.filenameExtRegexp);
|
|
155
167
|
let mappedFilePreviewType: FilePreviewTypes | typeof USE_EXT_MAPPING | false = USE_EXT_MAPPING;
|
|
156
168
|
|
|
@@ -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
|
package/vite.config.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve } from 'path';
|
|
2
2
|
import { defineConfig, UserConfig } from 'vite';
|
|
3
3
|
import { compression } from 'vite-plugin-compression2';
|
|
4
|
-
import dts from 'vite-plugin-dts';
|
|
4
|
+
// import dts from 'vite-plugin-dts';
|
|
5
5
|
|
|
6
6
|
// if NODE_DEV defined properly, uset it. otherwise set it to production.
|
|
7
7
|
const nodeDevs = ['production', 'development'];
|
|
@@ -13,19 +13,6 @@ const isDev = mode === 'development';
|
|
|
13
13
|
|
|
14
14
|
export default defineConfig(async (): Promise<UserConfig> => {
|
|
15
15
|
const plugins = [
|
|
16
|
-
/**
|
|
17
|
-
* generate TypeScript declaration files
|
|
18
|
-
*/
|
|
19
|
-
dts({
|
|
20
|
-
include: ['src/**/*'],
|
|
21
|
-
exclude: ['js/**/*', 'test/**/*', 'vendor/**/*'],
|
|
22
|
-
outDir: 'dist',
|
|
23
|
-
rollupTypes: true, // bundle all .d.ts files into a single ermrest.d.ts
|
|
24
|
-
compilerOptions: {
|
|
25
|
-
skipLibCheck: true,
|
|
26
|
-
},
|
|
27
|
-
logLevel: 'silent', // suppress warnings about legacy JS files
|
|
28
|
-
}),
|
|
29
16
|
/**
|
|
30
17
|
* generate the *.js.gz files so server can directly serve them
|
|
31
18
|
*/
|