Stackedit/src/services/utils.js
2022-11-20 15:11:31 +08:00

439 lines
13 KiB
JavaScript

import yaml from 'js-yaml';
import '../libs/clunderscore';
import presets from '../data/presets';
import constants from '../data/constants';
// For utils.uid()
const uidLength = 16;
const crypto = window.crypto || window.msCrypto;
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
const radix = alphabet.length;
const array = new Uint32Array(uidLength);
// For utils.parseQueryParams()
const parseQueryParams = (params) => {
const result = {};
params.split('&').forEach((param) => {
const [key, value] = param.split('=').map(decodeURIComponent);
if (key && value != null) {
result[key] = value;
}
});
return result;
};
// For utils.setQueryParams()
const filterParams = (params = {}) => {
const result = {};
Object.entries(params).forEach(([key, value]) => {
if (key && value != null) {
result[key] = value;
}
});
return result;
};
// For utils.computeProperties()
const deepOverride = (obj, opt) => {
if (obj === undefined) {
return opt;
}
const objType = Object.prototype.toString.call(obj);
const optType = Object.prototype.toString.call(opt);
if (objType !== optType) {
return obj;
}
if (objType !== '[object Object]') {
return opt === undefined ? obj : opt;
}
Object.keys({
...obj,
...opt,
}).forEach((key) => {
obj[key] = deepOverride(obj[key], opt[key]);
});
return obj;
};
// For utils.addQueryParams()
const urlParser = document.createElement('a');
const deepCopy = (obj) => {
if (obj == null) {
return obj;
}
return JSON.parse(JSON.stringify(obj));
};
// Compute presets
const computedPresets = {};
Object.keys(presets).forEach((key) => {
let preset = deepCopy(presets[key][0]);
if (presets[key][1]) {
preset = deepOverride(preset, presets[key][1]);
}
computedPresets[key] = preset;
});
export default {
computedPresets,
queryParams: parseQueryParams(window.location.hash.slice(1)),
setQueryParams(params = {}) {
this.queryParams = filterParams(params);
const serializedParams = Object.entries(this.queryParams).map(([key, value]) =>
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
const hash = `#${serializedParams}`;
if (window.location.hash !== hash) {
window.location.replace(hash);
}
},
sanitizeText(text) {
const result = `${text || ''}`.slice(0, constants.textMaxLength);
// last char must be a `\n`.
return `${result}\n`.replace(/\n\n$/, '\n');
},
sanitizeName(name) {
return `${name || ''}`
// Keep only 250 characters
.slice(0, 250) || constants.defaultName;
},
sanitizeFilename(name) {
return this.sanitizeName(`${name || ''}`
// Replace `/`, control characters and other kind of spaces with a space
.replace(/[/\x00-\x1F\x7f-\xa0\s]+/g, ' ') // eslint-disable-line no-control-regex
.trim()) || constants.defaultName;
},
deepCopy,
serializeObject(obj) {
return obj === undefined ? obj : JSON.stringify(obj, (key, value) => {
if (Object.prototype.toString.call(value) !== '[object Object]') {
return value;
}
// Sort keys to have a predictable result
return Object.keys(value).sort().reduce((sorted, valueKey) => {
sorted[valueKey] = value[valueKey];
return sorted;
}, {});
});
},
search(items, criteria) {
let result;
items.some((item) => {
// If every field fits the criteria
if (Object.entries(criteria).every(([key, value]) => value === item[key])) {
result = item;
}
return result;
});
return result;
},
uid() {
crypto.getRandomValues(array);
return array.cl_map(value => alphabet[value % radix]).join('');
},
hash(str) {
// https://stackoverflow.com/a/7616484/1333165
let hash = 0;
if (!str) return hash;
for (let i = 0; i < str.length; i += 1) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char; // eslint-disable-line no-bitwise
hash |= 0; // eslint-disable-line no-bitwise
}
return hash;
},
getItemHash(item) {
return this.hash(this.serializeObject({
...item,
// These properties must not be part of the hash
id: undefined,
hash: undefined,
history: undefined,
}));
},
addItemHash(item) {
return {
...item,
hash: this.getItemHash(item),
};
},
makeWorkspaceId(params) {
return Math.abs(this.hash(this.serializeObject(params))).toString(36);
},
getDbName(workspaceId) {
let dbName = 'stackedit-db';
if (workspaceId !== 'main') {
dbName += `-${workspaceId}`;
}
return dbName;
},
encodeBase64(str, urlSafe = false) {
const uriEncodedStr = encodeURIComponent(str);
const utf8Str = uriEncodedStr.replace(
/%([0-9A-F]{2})/g,
(match, p1) => String.fromCharCode(`0x${p1}`),
);
const result = btoa(utf8Str);
if (!urlSafe) {
return result;
}
return result
.replace(/\//g, '_') // Replace `/` with `_`
.replace(/\+/g, '-') // Replace `+` with `-`
.replace(/=+$/, ''); // Remove trailing `=`
},
encodeFiletoBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result.split(',').pop());
reader.onerror = error => reject(error);
});
},
base64ToBlob(dataurl, fileName) {
const potIdx = fileName.lastIndexOf('.');
const suffix = potIdx > -1 ? fileName.substring(potIdx + 1) : 'png';
const mime = `image/${suffix}`;
const bstr = atob(dataurl);
let n = bstr.length;
const u8arr = new Uint8Array(n);
while (n >= 0) {
n -= 1;
u8arr[n] = bstr.charCodeAt(n);
}
return new Blob([u8arr], { type: mime });
},
decodeBase64(str) {
// In case of URL safe base64
const sanitizedStr = str.replace(/_/g, '/').replace(/-/g, '+');
const utf8Str = atob(sanitizedStr);
const uriEncodedStr = utf8Str
.split('')
.map(c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`)
.join('');
return decodeURIComponent(uriEncodedStr);
},
computeProperties(yamlProperties) {
let properties = {};
try {
properties = yaml.safeLoad(yamlProperties) || {};
} catch (e) {
// Ignore
}
const extensions = properties.extensions || {};
const computedPreset = deepCopy(computedPresets[extensions.preset] || computedPresets.default);
const computedExtensions = deepOverride(computedPreset, properties.extensions);
computedExtensions.preset = extensions.preset;
properties.extensions = computedExtensions;
return properties;
},
randomize(value) {
return Math.floor((1 + (Math.random() * 0.2)) * value);
},
setInterval(func, interval) {
return setInterval(() => func(), this.randomize(interval));
},
async awaitSequence(values, asyncFunc) {
const results = [];
const valuesLeft = values.slice().reverse();
const runWithNextValue = async () => {
if (!valuesLeft.length) {
return results;
}
results.push(await asyncFunc(valuesLeft.pop()));
return runWithNextValue();
};
return runWithNextValue();
},
async awaitSome(asyncFunc) {
if (await asyncFunc()) {
return this.awaitSome(asyncFunc);
}
return null;
},
someResult(values, func) {
let result;
values.some((value) => {
result = func(value);
return result;
});
return result;
},
parseQueryParams,
addQueryParams(url = '', params = {}, hash = false) {
const keys = Object.keys(params).filter(key => params[key] != null);
urlParser.href = url;
if (!keys.length) {
return urlParser.href;
}
const serializedParams = keys.map(key =>
`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');
if (hash) {
if (urlParser.hash) {
urlParser.hash += '&';
} else {
urlParser.hash = '#';
}
urlParser.hash += serializedParams;
} else {
if (urlParser.search) {
urlParser.search += '&';
} else {
urlParser.search = '?';
}
urlParser.search += serializedParams;
}
return urlParser.href;
},
resolveUrl(baseUrl, path) {
const oldBaseElt = document.getElementsByTagName('base')[0];
const oldHref = oldBaseElt && oldBaseElt.href;
const newBaseElt = oldBaseElt || document.head.appendChild(document.createElement('base'));
newBaseElt.href = baseUrl;
urlParser.href = path;
const result = urlParser.href;
if (oldBaseElt) {
oldBaseElt.href = oldHref;
} else {
document.head.removeChild(newBaseElt);
}
return result;
},
getHostname(url) {
urlParser.href = url;
return urlParser.hostname;
},
encodeUrlPath(path) {
return path ? path.split('/').map(encodeURIComponent).join('/') : '';
},
decodeUrlPath(path) {
return path ? path.split('/').map(decodeURIComponent).join('/') : '';
},
parseGithubRepoUrl(url) {
const parsedRepo = url && url.match(/([^/:]+)\/([^/]+?)(?:\.git|\/)?$/);
return parsedRepo && {
owner: parsedRepo[1],
repo: parsedRepo[2],
};
},
parseGitlabProjectPath(url) {
const parsedProject = url && url.match(/^https:\/\/[^/]+\/(.+?)(?:\.git|\/)?$/);
return parsedProject && parsedProject[1];
},
parseGiteaProjectPath(url) {
const parsedProject = url && url.match(/^http[s]?:\/\/[^/]+\/(.+?)(?:\.git|\/)?$/);
return parsedProject && parsedProject[1];
},
createHiddenIframe(url) {
const iframeElt = document.createElement('iframe');
iframeElt.style.position = 'absolute';
iframeElt.style.left = '-99px';
iframeElt.style.width = '1px';
iframeElt.style.height = '1px';
iframeElt.src = url;
return iframeElt;
},
wrapRange(range, eltProperties) {
const rangeLength = `${range}`.length;
let wrappedLength = 0;
const treeWalker = document
.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_TEXT);
let { startOffset } = range;
treeWalker.currentNode = range.startContainer;
if (treeWalker.currentNode.nodeType === Node.TEXT_NODE || treeWalker.nextNode()) {
do {
if (treeWalker.currentNode.nodeValue !== '\n') {
if (treeWalker.currentNode === range.endContainer &&
range.endOffset < treeWalker.currentNode.nodeValue.length
) {
treeWalker.currentNode.splitText(range.endOffset);
}
if (startOffset) {
treeWalker.currentNode = treeWalker.currentNode.splitText(startOffset);
startOffset = 0;
}
const elt = document.createElement('span');
Object.entries(eltProperties).forEach(([key, value]) => {
elt[key] = value;
});
treeWalker.currentNode.parentNode.insertBefore(elt, treeWalker.currentNode);
elt.appendChild(treeWalker.currentNode);
}
wrappedLength += treeWalker.currentNode.nodeValue.length;
if (wrappedLength >= rangeLength) {
break;
}
}
while (treeWalker.nextNode());
}
},
unwrapRange(eltCollection) {
Array.prototype.slice.call(eltCollection).forEach((elt) => {
// Loop in case another wrapper has been added inside
for (let child = elt.firstChild; child; child = elt.firstChild) {
if (child.nodeType === 3) {
if (elt.previousSibling && elt.previousSibling.nodeType === 3) {
child.nodeValue = elt.previousSibling.nodeValue + child.nodeValue;
elt.parentNode.removeChild(elt.previousSibling);
}
if (!child.nextSibling && elt.nextSibling && elt.nextSibling.nodeType === 3) {
child.nodeValue += elt.nextSibling.nodeValue;
elt.parentNode.removeChild(elt.nextSibling);
}
}
elt.parentNode.insertBefore(child, elt);
}
elt.parentNode.removeChild(elt);
});
},
getAbsoluteDir(currDirNode) {
if (!currDirNode) {
return '';
}
let path = currDirNode.item.name;
if (currDirNode.parentNode) {
const parentPath = this.getAbsoluteDir(currDirNode.parentNode);
if (parentPath) {
path = `${parentPath}/${path}`;
}
}
return path || '';
},
// 根据当前绝对路径 与 文件路径计算出文件绝对路径
getAbsoluteFilePath(currDirNode, filePath) {
const currAbsolutePath = this.getAbsoluteDir(currDirNode);
// "/"开头说明已经是绝对路径
if (filePath.indexOf('/') === 0) {
return filePath.replaceAll(' ', '%20');
}
let path = filePath;
// 相对上级路径
if (path.indexOf('../') === 0) {
return this.getAbsoluteFilePath(currDirNode && currDirNode.parentNode, path.replace('../', ''));
} else if (path.indexOf('./') === 0) {
path = `${currAbsolutePath}/${path.replace('./', '')}`;
} else {
path = `${currAbsolutePath}/${path}`;
}
return (path.indexOf('/') === 0 ? path : `/${path}`).replaceAll(' ', '%20');
},
findNodeByPath(rootNode, currDirNode, filePath) {
// 先获取绝对路径
const path = this.getAbsoluteFilePath(currDirNode, filePath).replaceAll('%20', ' ');
const pathArr = path.split('/');
let node = rootNode;
for (let i = 0; i < pathArr.length; i += 1) {
if (i > 0) {
if (i === pathArr.length - 1) {
return node.files.find(it => `${it.item.name}.md` === pathArr[i]);
}
node = node.folders.find(it => it.item.name === pathArr[i]);
if (!node) {
return null;
}
}
}
return null;
},
};