-
Notifications
You must be signed in to change notification settings - Fork 12
/
index.js
186 lines (170 loc) · 6.02 KB
/
index.js
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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
/**
* @module index
* (Base script)
*/
/**
* Default languages supported
* @typedef {('asm'|'bash'|'bf'|'c'|'css'|'csv'|'diff'|'docker'|'git'|'go'|'html'|'http'|'ini'|'java'|'js'|'jsdoc'|'json'|'leanpub-md'|'log'|'lua'|'make'|'md'|'pl'|'plain'|'py'|'regex'|'rs'|'sql'|'todo'|'toml'|'ts'|'uri'|'xml'|'yaml')} ShjLanguage
*/
/**
* Themes supported in the browser
* @typedef {('atom-dark'|'github-dark'|'github-dim'|'dark'|'default'|'github-light'|'visual-studio-dark')} ShjBrowserTheme
*/
/**
* @typedef {Object} ShjOptions
* @property {Boolean} [hideLineNumbers=false] Indicates whether to hide line numbers
*/
/**
* @typedef {('inline'|'oneline'|'multiline')} ShjDisplayMode
* * `inline` inside `code` element
* * `oneline` inside `div` element and containing only one line
* * `multiline` inside `div` element
*/
/**
* Token types
* @typedef {('deleted'|'err'|'var'|'section'|'kwd'|'class'|'cmnt'|'insert'|'type'|'func'|'bool'|'num'|'oper'|'str'|'esc')} ShjToken
*/
import expandData from './common.js';
const langs = {},
sanitize = (str = '') =>
str.replaceAll('&', '&').replaceAll?.('<', '<').replaceAll?.('>', '>'),
/**
* Create a HTML element with the right token styling
*
* @function
* @ignore
* @param {string} str The content (need to be sanitized)
* @param {ShjToken} [token] The type of token
* @returns A HMTL string
*/
toSpan = (str, token) => token ? `<span class="shj-syn-${token}">${str}</span>` : str;
/**
* Find the tokens in the given code and call the given callback
*
* @function tokenize
* @param {string} src The code
* @param {ShjLanguage|Array} lang The language of the code
* @param {function(string, ShjToken=):void} token The callback function
* this function will be given
* * the text of the token
* * the type of the token
*/
export async function tokenize(src, lang, token) {
try {
let m,
part,
first = {},
match,
cache = [],
i = 0,
data = typeof lang === 'string' ? (await (langs[lang] ??= import(`./languages/${lang}.js`))) : lang,
// make a fast shallow copy to bee able to splice lang without change the original one
arr = [...typeof lang === 'string' ? data.default : lang.sub];
while (i < src.length) {
first.index = null;
for (m = arr.length; m-- > 0;) {
part = arr[m].expand ? expandData[arr[m].expand] : arr[m];
// do not call again exec if the previous result is sufficient
if (cache[m] === undefined || cache[m].match.index < i) {
part.match.lastIndex = i;
match = part.match.exec(src);
if (match === null) {
// no more match with this regex can be disposed
arr.splice(m, 1);
cache.splice(m, 1);
continue;
}
// save match for later use to decrease performance cost
cache[m] = { match, lastIndex: part.match.lastIndex };
}
// check if it the first match in the string
if (cache[m].match[0] && (cache[m].match.index <= first.index || first.index === null))
first = {
part: part,
index: cache[m].match.index,
match: cache[m].match[0],
end: cache[m].lastIndex
}
}
if (first.index === null)
break;
token(src.slice(i, first.index), data.type);
i = first.end;
if (first.part.sub)
await tokenize(first.match, typeof first.part.sub === 'string' ? first.part.sub : (typeof first.part.sub === 'function' ? first.part.sub(first.match) : first.part), token);
else
token(first.match, first.part.type);
}
token(src.slice(i, src.length), data.type);
}
catch {
token(src);
}
}
/**
* Highlight a string passed as argument and return it
* @example
* elm.innerHTML = await highlightText(code, 'js');
*
* @async
* @function highlightText
* @param {string} src The code
* @param {ShjLanguage} lang The language of the code
* @param {Boolean} [multiline=true] If it is multiline, it will add a wrapper for the line numbering and header
* @param {ShjOptions} [opt={}] Customization options
* @returns {Promise<string>} The highlighted string
*/
export async function highlightText(src, lang, multiline = true, opt = {}) {
let tmp = ''
await tokenize(src, lang, (str, type) => tmp += toSpan(sanitize(str), type))
return multiline
? `<div><div class="shj-numbers">${'<div></div>'.repeat(!opt.hideLineNumbers && src.split('\n').length)}</div><div>${tmp}</div></div>`
: tmp;
}
/**
* Highlight a DOM element by getting the new innerHTML with highlightText
*
* @async
* @function highlightElement
* @param {Element} elm The DOM element
* @param {ShjLanguage} [lang] The language of the code (seaching by default on `elm` for a 'shj-lang-' class)
* @param {ShjDisplayMode} [mode] The display mode (guessed by default)
* @param {ShjOptions} [opt={}] Customization options
*/
export async function highlightElement(elm, lang = elm.className.match(/shj-lang-([\w-]+)/)?.[1], mode, opt) {
let txt = elm.textContent;
mode ??= `${elm.tagName == 'CODE' ? 'in' : (txt.split('\n').length < 2 ? 'one' : 'multi')}line`;
elm.dataset.lang = lang;
elm.className = `${[...elm.classList].filter(className => !className.startsWith('shj-')).join(' ')} shj-lang-${lang} shj-${mode}`;
elm.innerHTML = await highlightText(txt, lang, mode == 'multiline', opt);
}
/**
* Call highlightElement on element with a css class starting with `shj-lang-`
*
* @async
* @function highlightAll
* @param {ShjOptions} [opt={}] Customization options
*/
export let highlightAll = async (opt) =>
document
.querySelectorAll('[class*="shj-lang-"]')
.forEach(elm => highlightElement(elm, undefined, undefined, opt))
/**
* @typedef {{ match: RegExp, type: string }
* | { match: RegExp, sub: string | ShjLanguageDefinition | (code:string) => ShjLanguageComponent }
* | { expand: string }
* } ShjLanguageComponent
*/
/**
* @typedef {ShjLanguageComponent[]} ShjLanguageDefinition
*/
/**
* Load a language and add it to the langs object
*
* @function loadLanguage
* @param {string} languageName The name of the language
* @param {{ default: ShjLanguageDefinition }} language The language
*/
export let loadLanguage = (languageName, language) => {
langs[languageName] = language;
}