mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-24 10:27:10 +02:00
Chore: Refactor renderer package: Limit dependency on @joplin/lib
and improve type safety (#9701)
This commit is contained in:
parent
352ee6496e
commit
f5e1e45f6f
@ -734,6 +734,7 @@ packages/lib/models/utils/itemCanBeEncrypted.js
|
||||
packages/lib/models/utils/paginatedFeed.js
|
||||
packages/lib/models/utils/paginationToSql.js
|
||||
packages/lib/models/utils/readOnly.js
|
||||
packages/lib/models/utils/resourceUtils.js
|
||||
packages/lib/models/utils/types.js
|
||||
packages/lib/models/utils/userData.test.js
|
||||
packages/lib/models/utils/userData.js
|
||||
@ -1056,13 +1057,15 @@ packages/renderer/MdToHtml/rules/sanitize_html.js
|
||||
packages/renderer/MdToHtml/rules/source_map.js
|
||||
packages/renderer/MdToHtml/setupLinkify.js
|
||||
packages/renderer/MdToHtml/validateLinks.js
|
||||
packages/renderer/assetsToHeaders.js
|
||||
packages/renderer/defaultResourceModel.js
|
||||
packages/renderer/headerAnchor.js
|
||||
packages/renderer/highlight.js
|
||||
packages/renderer/htmlUtils.test.js
|
||||
packages/renderer/htmlUtils.js
|
||||
packages/renderer/index.js
|
||||
packages/renderer/noteStyle.js
|
||||
packages/renderer/pathUtils.js
|
||||
packages/renderer/types.js
|
||||
packages/renderer/utils.js
|
||||
packages/tools/build-release-stats.test.js
|
||||
packages/tools/build-release-stats.js
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -714,6 +714,7 @@ packages/lib/models/utils/itemCanBeEncrypted.js
|
||||
packages/lib/models/utils/paginatedFeed.js
|
||||
packages/lib/models/utils/paginationToSql.js
|
||||
packages/lib/models/utils/readOnly.js
|
||||
packages/lib/models/utils/resourceUtils.js
|
||||
packages/lib/models/utils/types.js
|
||||
packages/lib/models/utils/userData.test.js
|
||||
packages/lib/models/utils/userData.js
|
||||
@ -1036,13 +1037,15 @@ packages/renderer/MdToHtml/rules/sanitize_html.js
|
||||
packages/renderer/MdToHtml/rules/source_map.js
|
||||
packages/renderer/MdToHtml/setupLinkify.js
|
||||
packages/renderer/MdToHtml/validateLinks.js
|
||||
packages/renderer/assetsToHeaders.js
|
||||
packages/renderer/defaultResourceModel.js
|
||||
packages/renderer/headerAnchor.js
|
||||
packages/renderer/highlight.js
|
||||
packages/renderer/htmlUtils.test.js
|
||||
packages/renderer/htmlUtils.js
|
||||
packages/renderer/index.js
|
||||
packages/renderer/noteStyle.js
|
||||
packages/renderer/pathUtils.js
|
||||
packages/renderer/types.js
|
||||
packages/renderer/utils.js
|
||||
packages/tools/build-release-stats.test.js
|
||||
packages/tools/build-release-stats.js
|
||||
|
@ -1,5 +1,6 @@
|
||||
|
||||
import MarkupToHtml, { MarkupLanguage, RenderResult } from '@joplin/renderer/MarkupToHtml';
|
||||
import MarkupToHtml, { MarkupLanguage } from '@joplin/renderer/MarkupToHtml';
|
||||
import { RenderResult } from '@joplin/renderer/types';
|
||||
|
||||
describe('MarkupToHtml', () => {
|
||||
|
||||
|
@ -10,7 +10,7 @@ import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigSc
|
||||
import { reg } from '@joplin/lib/registry';
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('@joplin/lib/theme');
|
||||
const pathUtils = require('@joplin/lib/path-utils');
|
||||
import * as pathUtils from '@joplin/lib/path-utils';
|
||||
import SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
|
||||
import * as shared from '@joplin/lib/components/shared/config/config-shared.js';
|
||||
import ClipperConfigScreen from '../ClipperConfigScreen';
|
||||
|
@ -2,7 +2,7 @@ import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
|
||||
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
|
||||
import { PluginStates } from '@joplin/lib/services/plugins/reducer';
|
||||
import { MarkupLanguage } from '@joplin/renderer';
|
||||
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/MarkupToHtml';
|
||||
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/types';
|
||||
import { MarkupToHtmlOptions } from './useMarkupToHtml';
|
||||
import { Dispatch } from 'redux';
|
||||
import { ProcessResultsRow } from '@joplin/lib/services/search/SearchEngine';
|
||||
|
@ -5,7 +5,7 @@ const { themeStyle } = require('../../global-style.js');
|
||||
import markupLanguageUtils from '@joplin/lib/markupLanguageUtils';
|
||||
import useEditPopup from './useEditPopup';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
const { assetsToHeaders } = require('@joplin/renderer');
|
||||
import { assetsToHeaders } from '@joplin/renderer';
|
||||
|
||||
const logger = Logger.create('NoteBodyViewer/useSource');
|
||||
|
||||
|
@ -7,9 +7,9 @@ import markdownUtils from '../markdownUtils';
|
||||
import { _ } from '../locale';
|
||||
import { ResourceEntity, ResourceLocalStateEntity, ResourceOcrStatus, SqlQuery } from '../services/database/types';
|
||||
import ResourceLocalState from './ResourceLocalState';
|
||||
const pathUtils = require('../path-utils');
|
||||
import * as pathUtils from '../path-utils';
|
||||
import { safeFilename } from '../path-utils';
|
||||
const { mime } = require('../mime-utils.js');
|
||||
const { filename, safeFilename } = require('../path-utils');
|
||||
const { FsDriverDummy } = require('../fs-driver-dummy.js');
|
||||
import JoplinError from '../JoplinError';
|
||||
import itemCanBeEncrypted from './utils/itemCanBeEncrypted';
|
||||
@ -23,6 +23,7 @@ import { RecognizeResultLine } from '../services/ocr/utils/types';
|
||||
import eventManager, { EventName } from '../eventManager';
|
||||
import { unique } from '../array';
|
||||
import isSqliteSyntaxError from '../services/database/isSqliteSyntaxError';
|
||||
import { internalUrl, isResourceUrl, isSupportedImageMimeType, resourceFilename, resourceFullPath, resourcePathToId, resourceRelativePath, resourceUrlToId } from './utils/resourceUtils';
|
||||
|
||||
export default class Resource extends BaseItem {
|
||||
|
||||
@ -56,8 +57,7 @@ export default class Resource extends BaseItem {
|
||||
}
|
||||
|
||||
public static isSupportedImageMimeType(type: string) {
|
||||
const imageMimeTypes = ['image/jpg', 'image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp', 'image/avif'];
|
||||
return imageMimeTypes.indexOf(type.toLowerCase()) >= 0;
|
||||
return isSupportedImageMimeType(type);
|
||||
}
|
||||
|
||||
public static fetchStatuses(resourceIds: string[]): Promise<any[]> {
|
||||
@ -121,10 +121,7 @@ export default class Resource extends BaseItem {
|
||||
}
|
||||
|
||||
public static filename(resource: ResourceEntity, encryptedBlob = false) {
|
||||
let extension = encryptedBlob ? 'crypted' : resource.file_extension;
|
||||
if (!extension) extension = resource.mime ? mime.toFileExtension(resource.mime) : '';
|
||||
extension = extension ? `.${extension}` : '';
|
||||
return resource.id + extension;
|
||||
return resourceFilename(resource, encryptedBlob);
|
||||
}
|
||||
|
||||
public static friendlySafeFilename(resource: ResourceEntity) {
|
||||
@ -137,11 +134,11 @@ export default class Resource extends BaseItem {
|
||||
}
|
||||
|
||||
public static relativePath(resource: ResourceEntity, encryptedBlob = false) {
|
||||
return `${Setting.value('resourceDirName')}/${this.filename(resource, encryptedBlob)}`;
|
||||
return resourceRelativePath(resource, this.baseRelativeDirectoryPath(), encryptedBlob);
|
||||
}
|
||||
|
||||
public static fullPath(resource: ResourceEntity, encryptedBlob = false) {
|
||||
return `${Setting.value('resourceDir')}/${this.filename(resource, encryptedBlob)}`;
|
||||
return resourceFullPath(resource, this.baseDirectoryPath(), encryptedBlob);
|
||||
}
|
||||
|
||||
public static async isReady(resource: ResourceEntity) {
|
||||
@ -270,11 +267,11 @@ export default class Resource extends BaseItem {
|
||||
}
|
||||
|
||||
public static internalUrl(resource: ResourceEntity) {
|
||||
return `:/${resource.id}`;
|
||||
return internalUrl(resource);
|
||||
}
|
||||
|
||||
public static pathToId(path: string) {
|
||||
return filename(path);
|
||||
return resourcePathToId(path);
|
||||
}
|
||||
|
||||
public static async content(resource: ResourceEntity) {
|
||||
@ -282,12 +279,11 @@ export default class Resource extends BaseItem {
|
||||
}
|
||||
|
||||
public static isResourceUrl(url: string) {
|
||||
return url && url.length === 34 && url[0] === ':' && url[1] === '/';
|
||||
return isResourceUrl(url);
|
||||
}
|
||||
|
||||
public static urlToId(url: string) {
|
||||
if (!this.isResourceUrl(url)) throw new Error(`Not a valid resource URL: ${url}`);
|
||||
return url.substr(2);
|
||||
return resourceUrlToId(url);
|
||||
}
|
||||
|
||||
public static async localState(resourceOrId: any): Promise<ResourceLocalStateEntity> {
|
||||
|
43
packages/lib/models/utils/resourceUtils.ts
Normal file
43
packages/lib/models/utils/resourceUtils.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import type { ResourceEntity } from '../../services/database/types';
|
||||
const { mime } = require('../../mime-utils.js');
|
||||
import { filename } from '@joplin/utils/path';
|
||||
|
||||
// This file contains resource-related utilities that do not
|
||||
// depend on the database, settings, etc.
|
||||
|
||||
export const resourceFilename = (resource: ResourceEntity, encryptedBlob = false) => {
|
||||
let extension = encryptedBlob ? 'crypted' : resource.file_extension;
|
||||
if (!extension) extension = resource.mime ? mime.toFileExtension(resource.mime) : '';
|
||||
extension = extension ? `.${extension}` : '';
|
||||
return resource.id + extension;
|
||||
};
|
||||
|
||||
export const resourceRelativePath = (resource: ResourceEntity, relativeResourceDirPath: string, encryptedBlob = false) => {
|
||||
return `${relativeResourceDirPath}/${resourceFilename(resource, encryptedBlob)}`;
|
||||
};
|
||||
|
||||
export const resourceFullPath = (resource: ResourceEntity, resourceDirPath: string, encryptedBlob = false) => {
|
||||
return `${resourceDirPath}/${resourceFilename(resource, encryptedBlob)}`;
|
||||
};
|
||||
|
||||
export const internalUrl = (resource: ResourceEntity) => {
|
||||
return `:/${resource.id}`;
|
||||
};
|
||||
|
||||
export const resourcePathToId = (path: string) => {
|
||||
return filename(path);
|
||||
};
|
||||
|
||||
export const isResourceUrl = (url: string) => {
|
||||
return url && url.length === 34 && url[0] === ':' && url[1] === '/';
|
||||
};
|
||||
|
||||
export const resourceUrlToId = (url: string) => {
|
||||
if (!isResourceUrl(url)) throw new Error(`Not a valid resource URL: ${url}`);
|
||||
return url.substring(2);
|
||||
};
|
||||
|
||||
export const isSupportedImageMimeType = (type: string) => {
|
||||
const imageMimeTypes = ['image/jpg', 'image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp', 'image/avif'];
|
||||
return imageMimeTypes.indexOf(type.toLowerCase()) >= 0;
|
||||
};
|
@ -1,65 +1,8 @@
|
||||
/* eslint no-useless-escape: 0*/
|
||||
|
||||
const { _ } = require('./locale');
|
||||
|
||||
export function dirname(path: string) {
|
||||
if (!path) throw new Error('Path is empty');
|
||||
const s = path.split(/\/|\\/);
|
||||
s.pop();
|
||||
return s.join('/');
|
||||
}
|
||||
|
||||
export function basename(path: string) {
|
||||
if (!path) throw new Error('Path is empty');
|
||||
const s = path.split(/\/|\\/);
|
||||
return s[s.length - 1];
|
||||
}
|
||||
|
||||
export function filename(path: string, includeDir = false) {
|
||||
if (!path) throw new Error('Path is empty');
|
||||
const output = includeDir ? path : basename(path);
|
||||
if (output.indexOf('.') < 0) return output;
|
||||
|
||||
const splitted = output.split('.');
|
||||
splitted.pop();
|
||||
return splitted.join('.');
|
||||
}
|
||||
|
||||
export function fileExtension(path: string) {
|
||||
if (!path) throw new Error('Path is empty');
|
||||
|
||||
const output = path.split('.');
|
||||
if (output.length <= 1) return '';
|
||||
return output[output.length - 1];
|
||||
}
|
||||
|
||||
export function isHidden(path: string) {
|
||||
const b = basename(path);
|
||||
if (!b.length) throw new Error(`Path empty or not a valid path: ${path}`);
|
||||
return b[0] === '.';
|
||||
}
|
||||
|
||||
// Note that this function only sanitizes a file extension - it does NOT extract
|
||||
// the file extension from a filename. So the way you'd normally call this is
|
||||
// `safeFileExtension(fileExtension(filename))`
|
||||
export function safeFileExtension(e: string, maxLength: number = null) {
|
||||
// In theory the file extension can have any length but in practice Joplin
|
||||
// expects a fixed length, so we limit it to 20 which should cover most cases.
|
||||
// Note that it means that a file extension longer than 20 will break
|
||||
// external editing (since the extension would be truncated).
|
||||
// https://discourse.joplinapp.org/t/troubles-with-webarchive-files-on-ios/10447
|
||||
if (maxLength === null) maxLength = 20;
|
||||
if (!e || !e.replace) return '';
|
||||
return e.replace(/[^a-zA-Z0-9]/g, '').substr(0, maxLength);
|
||||
}
|
||||
|
||||
export function safeFilename(e: string, maxLength: number = null, allowSpaces = false) {
|
||||
if (maxLength === null) maxLength = 32;
|
||||
if (!e || !e.replace) return '';
|
||||
const regex = allowSpaces ? /[^a-zA-Z0-9\-_\(\)\. ]/g : /[^a-zA-Z0-9\-_\(\)\.]/g;
|
||||
const output = e.replace(regex, '_');
|
||||
return output.substr(0, maxLength);
|
||||
}
|
||||
import { _ } from './locale';
|
||||
import { fileExtension, filename, safeFileExtension } from '@joplin/utils/path';
|
||||
export * from '@joplin/utils/path';
|
||||
|
||||
let friendlySafeFilename_blackListChars = '/\n\r<>:\'"\\|?*#';
|
||||
for (let i = 0; i < 32; i++) {
|
||||
@ -129,77 +72,3 @@ export function friendlySafeFilename(e: string, maxLength: number = null, preser
|
||||
|
||||
return output.substr(0, maxLength) + fileExt;
|
||||
}
|
||||
|
||||
export function toFileProtocolPath(filePathEncode: string, os: string = null) {
|
||||
if (os === null) os = process.platform;
|
||||
|
||||
if (os === 'win32') {
|
||||
filePathEncode = filePathEncode.replace(/\\/g, '/'); // replace backslash in windows pathname with slash e.g. c:\temp to c:/temp
|
||||
filePathEncode = `/${filePathEncode}`; // put slash in front of path to comply with windows fileURL syntax
|
||||
}
|
||||
|
||||
filePathEncode = encodeURI(filePathEncode);
|
||||
filePathEncode = filePathEncode.replace(/\+/g, '%2B'); // escape '+' with unicode
|
||||
return `file://${filePathEncode.replace(/\'/g, '%27')}`; // escape '(single quote) with unicode, to prevent crashing the html view
|
||||
}
|
||||
|
||||
export function toSystemSlashes(path: string, os: string = null) {
|
||||
if (os === null) os = process.platform;
|
||||
if (os === 'win32') return path.replace(/\//g, '\\');
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
export function toForwardSlashes(path: string) {
|
||||
return toSystemSlashes(path, 'linux');
|
||||
}
|
||||
|
||||
export function rtrimSlashes(path: string) {
|
||||
return path.replace(/[\/\\]+$/, '');
|
||||
}
|
||||
|
||||
export function ltrimSlashes(path: string) {
|
||||
return path.replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
export function trimSlashes(path: string): string {
|
||||
return ltrimSlashes(rtrimSlashes(path));
|
||||
}
|
||||
|
||||
export function quotePath(path: string) {
|
||||
if (!path) return '';
|
||||
if (path.indexOf('"') < 0 && path.indexOf(' ') < 0) return path;
|
||||
path = path.replace(/"/, '\\"');
|
||||
return `"${path}"`;
|
||||
}
|
||||
|
||||
export function unquotePath(path: string) {
|
||||
if (!path.length) return '';
|
||||
if (path.length && path[0] === '"') {
|
||||
path = path.substr(1, path.length - 2);
|
||||
}
|
||||
path = path.replace(/\\"/, '"');
|
||||
return path;
|
||||
}
|
||||
|
||||
export function extractExecutablePath(cmd: string) {
|
||||
if (!cmd.length) return '';
|
||||
|
||||
const quoteType = ['"', '\''].indexOf(cmd[0]) >= 0 ? cmd[0] : '';
|
||||
|
||||
let output = '';
|
||||
for (let i = 0; i < cmd.length; i++) {
|
||||
const c = cmd[i];
|
||||
if (quoteType) {
|
||||
if (i > 0 && c === quoteType) {
|
||||
output += c;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (c === ' ') break;
|
||||
}
|
||||
|
||||
output += c;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
@ -1,9 +1,7 @@
|
||||
const { extractExecutablePath, quotePath, unquotePath, friendlySafeFilename, toFileProtocolPath } = require('./path-utils');
|
||||
const { friendlySafeFilename } = require('./path-utils');
|
||||
|
||||
describe('pathUtils', () => {
|
||||
|
||||
|
||||
|
||||
it('should create friendly safe filename', (async () => {
|
||||
const testCases = [
|
||||
['生活', '生活'],
|
||||
@ -32,59 +30,4 @@ describe('pathUtils', () => {
|
||||
expect(friendlySafeFilename('thatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylong.md', null, true)).toBe('thatsreallylongthatsreallylongthatsreallylongthats.md');
|
||||
}));
|
||||
|
||||
it('should quote and unquote paths', (async () => {
|
||||
const testCases = [
|
||||
['', ''],
|
||||
['/my/path', '/my/path'],
|
||||
['/my/path with spaces', '"/my/path with spaces"'],
|
||||
['/my/weird"path', '"/my/weird\\"path"'],
|
||||
['c:\\Windows\\test.dll', 'c:\\Windows\\test.dll'],
|
||||
['c:\\Windows\\test test.dll', '"c:\\Windows\\test test.dll"'],
|
||||
];
|
||||
|
||||
for (let i = 0; i < testCases.length; i++) {
|
||||
const t = testCases[i];
|
||||
expect(quotePath(t[0])).toBe(t[1]);
|
||||
expect(unquotePath(quotePath(t[0]))).toBe(t[0]);
|
||||
}
|
||||
}));
|
||||
|
||||
it('should extract executable path from command', (async () => {
|
||||
const testCases = [
|
||||
['', ''],
|
||||
['/my/cmd -some -args', '/my/cmd'],
|
||||
['"/my/cmd" -some -args', '"/my/cmd"'],
|
||||
['"/my/cmd"', '"/my/cmd"'],
|
||||
['"/my/cmd and space" -some -flags', '"/my/cmd and space"'],
|
||||
['"" -some -flags', '""'],
|
||||
];
|
||||
|
||||
for (let i = 0; i < testCases.length; i++) {
|
||||
const t = testCases[i];
|
||||
expect(extractExecutablePath(t[0])).toBe(t[1]);
|
||||
}
|
||||
}));
|
||||
|
||||
it('should create correct fileURL syntax', (async () => {
|
||||
const testCases_win32 = [
|
||||
['C:\\handle\\space test', 'file:///C:/handle/space%20test'],
|
||||
['C:\\escapeplus\\+', 'file:///C:/escapeplus/%2B'],
|
||||
['C:\\handle\\single quote\'', 'file:///C:/handle/single%20quote%27'],
|
||||
];
|
||||
const testCases_unixlike = [
|
||||
['/handle/space test', 'file:///handle/space%20test'],
|
||||
['/escapeplus/+', 'file:///escapeplus/%2B'],
|
||||
['/handle/single quote\'', 'file:///handle/single%20quote%27'],
|
||||
];
|
||||
|
||||
for (let i = 0; i < testCases_win32.length; i++) {
|
||||
const t = testCases_win32[i];
|
||||
expect(toFileProtocolPath(t[0], 'win32')).toBe(t[1]);
|
||||
}
|
||||
for (let i = 0; i < testCases_unixlike.length; i++) {
|
||||
const t = testCases_unixlike[i];
|
||||
expect(toFileProtocolPath(t[0], 'linux')).toBe(t[1]);
|
||||
}
|
||||
}));
|
||||
|
||||
});
|
||||
|
@ -12,7 +12,7 @@ import { basename, friendlySafeFilename, rtrimSlashes, dirname } from '../../pat
|
||||
import htmlpack from '@joplin/htmlpack';
|
||||
const { themeStyle } = require('../../theme');
|
||||
const { escapeHtml } = require('../../string-utils.js');
|
||||
const { assetsToHeaders } = require('@joplin/renderer');
|
||||
import { assetsToHeaders } from '@joplin/renderer';
|
||||
|
||||
export default class InteropService_Exporter_Html extends InteropService_Exporter_Base {
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PluginStates } from '../reducer';
|
||||
import { ContentScriptType, ContentScriptContext, PostMessageHandler, ContentScriptModule } from '../api/types';
|
||||
import { dirname } from '@joplin/renderer/pathUtils';
|
||||
import { dirname } from '@joplin/utils/path';
|
||||
import shim from '../../../shim';
|
||||
import Logger from '@joplin/utils/Logger';
|
||||
import PluginService from '../PluginService';
|
||||
|
@ -1,10 +1,10 @@
|
||||
import htmlUtils from './htmlUtils';
|
||||
import linkReplacement from './MdToHtml/linkReplacement';
|
||||
import utils, { ItemIdToUrlHandler } from './utils';
|
||||
import * as utils from './utils';
|
||||
import InMemoryCache from './InMemoryCache';
|
||||
import { RenderResult } from './MarkupToHtml';
|
||||
import noteStyle, { whiteBackgroundNoteStyle } from './noteStyle';
|
||||
import { Options as NoteStyleOptions } from './noteStyle';
|
||||
import { FsDriver, MarkupRenderer, OptionsResourceModel, RenderOptions, RenderResult } from './types';
|
||||
const md5 = require('md5');
|
||||
|
||||
// Renderered notes can potentially be quite large (for example
|
||||
@ -17,38 +17,12 @@ export interface SplittedHtml {
|
||||
css: string;
|
||||
}
|
||||
|
||||
interface FsDriver {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
writeFile: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
exists: Function;
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types -- Old code before rule was applied
|
||||
cacheCssToFile: Function;
|
||||
}
|
||||
|
||||
interface Options {
|
||||
ResourceModel: any;
|
||||
ResourceModel: OptionsResourceModel;
|
||||
resourceBaseUrl?: string;
|
||||
fsDriver?: FsDriver;
|
||||
}
|
||||
|
||||
interface RenderOptions {
|
||||
splitted: boolean;
|
||||
bodyOnly: boolean;
|
||||
externalAssetsOnly: boolean;
|
||||
resources: any;
|
||||
postMessageSyntax: string;
|
||||
enableLongPress: boolean;
|
||||
itemIdToUrl?: ItemIdToUrlHandler;
|
||||
allowedFilePrefixes?: string[];
|
||||
whiteBackgroundNoteRendering?: boolean;
|
||||
|
||||
// For compatibility with MdToHtml options:
|
||||
plugins?: {
|
||||
link_open?: { linkRenderingType?: number };
|
||||
};
|
||||
}
|
||||
|
||||
// https://github.com/es-shims/String.prototype.trimStart/blob/main/implementation.js
|
||||
function trimStart(s: string): string {
|
||||
// eslint-disable-next-line no-control-regex
|
||||
@ -56,12 +30,12 @@ function trimStart(s: string): string {
|
||||
return s.replace(startWhitespace, '');
|
||||
}
|
||||
|
||||
export default class HtmlToHtml {
|
||||
export default class HtmlToHtml implements MarkupRenderer {
|
||||
|
||||
private resourceBaseUrl_;
|
||||
private ResourceModel_;
|
||||
private cache_;
|
||||
private fsDriver_: any;
|
||||
private fsDriver_: FsDriver;
|
||||
|
||||
public constructor(options: Options = null) {
|
||||
options = {
|
||||
@ -101,6 +75,10 @@ export default class HtmlToHtml {
|
||||
return [await this.fsDriver().cacheCssToFile(cssStrings)];
|
||||
}
|
||||
|
||||
public clearCache(): void {
|
||||
// TODO: Clear the in-memory cache
|
||||
}
|
||||
|
||||
// Note: the "theme" variable is ignored and instead the light theme is
|
||||
// always used for HTML notes.
|
||||
// See: https://github.com/laurent22/joplin/issues/3698
|
||||
|
@ -3,6 +3,8 @@ import HtmlToHtml from './HtmlToHtml';
|
||||
import htmlUtils from './htmlUtils';
|
||||
import { Options as NoteStyleOptions } from './noteStyle';
|
||||
import { AllHtmlEntities } from 'html-entities';
|
||||
import { FsDriver, MarkupRenderer, MarkupToHtmlConverter, OptionsResourceModel, RenderResult } from './types';
|
||||
import defaultResourceModel from './defaultResourceModel';
|
||||
const MarkdownIt = require('markdown-it');
|
||||
|
||||
export enum MarkupLanguage {
|
||||
@ -11,29 +13,6 @@ export enum MarkupLanguage {
|
||||
Any = 3,
|
||||
}
|
||||
|
||||
export interface RenderResultPluginAsset {
|
||||
name: string;
|
||||
mime: string;
|
||||
path: string;
|
||||
|
||||
// For built-in Mardown-it plugins, the asset path is relative (and can be
|
||||
// found inside the @joplin/renderer package), while for external plugins
|
||||
// (content scripts), the path is absolute. We use this property to tell if
|
||||
// it's relative or absolute, as that will inform how it's loaded in various
|
||||
// places.
|
||||
pathIsAbsolute: boolean;
|
||||
}
|
||||
|
||||
export interface RenderResult {
|
||||
html: string;
|
||||
pluginAssets: RenderResultPluginAsset[];
|
||||
cssStrings: string[];
|
||||
}
|
||||
|
||||
export interface OptionsResourceModel {
|
||||
isResourceUrl: (url: string)=> boolean;
|
||||
}
|
||||
|
||||
export interface Options {
|
||||
isSafeMode?: boolean;
|
||||
ResourceModel?: OptionsResourceModel;
|
||||
@ -42,23 +21,21 @@ export interface Options {
|
||||
resourceBaseUrl?: string;
|
||||
pluginOptions?: any; // Not sure if needed
|
||||
tempDir?: string; // Not sure if needed
|
||||
fsDriver?: any; // Not sure if needed
|
||||
fsDriver?: FsDriver; // Not sure if needed
|
||||
}
|
||||
|
||||
export default class MarkupToHtml {
|
||||
export default class MarkupToHtml implements MarkupToHtmlConverter {
|
||||
|
||||
public static MARKUP_LANGUAGE_MARKDOWN: number = MarkupLanguage.Markdown;
|
||||
public static MARKUP_LANGUAGE_HTML: number = MarkupLanguage.Html;
|
||||
|
||||
private renderers_: any = {};
|
||||
private renderers_: Record<string, MarkupRenderer> = {};
|
||||
private options_: Options;
|
||||
private rawMarkdownIt_: any;
|
||||
|
||||
public constructor(options: Options = null) {
|
||||
this.options_ = {
|
||||
ResourceModel: {
|
||||
isResourceUrl: () => false,
|
||||
},
|
||||
ResourceModel: defaultResourceModel,
|
||||
isSafeMode: false,
|
||||
...options,
|
||||
};
|
||||
|
@ -1,11 +1,10 @@
|
||||
import InMemoryCache from './InMemoryCache';
|
||||
import noteStyle from './noteStyle';
|
||||
import { fileExtension } from './pathUtils';
|
||||
import { fileExtension } from '@joplin/utils/path';
|
||||
import setupLinkify from './MdToHtml/setupLinkify';
|
||||
import validateLinks from './MdToHtml/validateLinks';
|
||||
import { ItemIdToUrlHandler } from './utils';
|
||||
import { RenderResult, RenderResultPluginAsset } from './MarkupToHtml';
|
||||
import { Options as NoteStyleOptions } from './noteStyle';
|
||||
import { FsDriver, ItemIdToUrlHandler, MarkupRenderer, OptionsResourceModel, RenderOptions, RenderResult, RenderResultPluginAsset } from './types';
|
||||
import hljs from './highlight';
|
||||
import * as MarkdownIt from 'markdown-it';
|
||||
|
||||
@ -13,28 +12,6 @@ const Entities = require('html-entities').AllHtmlEntities;
|
||||
const htmlentities = new Entities().encode;
|
||||
const md5 = require('md5');
|
||||
|
||||
export interface RenderOptions {
|
||||
contentMaxWidth?: number;
|
||||
bodyOnly?: boolean;
|
||||
splitted?: boolean;
|
||||
externalAssetsOnly?: boolean;
|
||||
postMessageSyntax?: string;
|
||||
highlightedKeywords?: string[];
|
||||
codeTheme?: string;
|
||||
theme?: any;
|
||||
plugins?: Record<string, any>;
|
||||
audioPlayerEnabled?: boolean;
|
||||
videoPlayerEnabled?: boolean;
|
||||
pdfViewerEnabled?: boolean;
|
||||
codeHighlightCacheKey?: string;
|
||||
plainResourceRendering?: boolean;
|
||||
mapsToLine?: boolean;
|
||||
useCustomPdfViewer?: boolean;
|
||||
noteId?: string;
|
||||
vendorDir?: string;
|
||||
settingValue?: (pluginId: string, key: string)=> any;
|
||||
}
|
||||
|
||||
interface RendererRule {
|
||||
install(context: any, ruleOptions: any): any;
|
||||
assets?(theme: any): any;
|
||||
@ -109,10 +86,10 @@ export interface ExtraRendererRule {
|
||||
|
||||
export interface Options {
|
||||
resourceBaseUrl?: string;
|
||||
ResourceModel?: any;
|
||||
ResourceModel?: OptionsResourceModel;
|
||||
pluginOptions?: any;
|
||||
tempDir?: string;
|
||||
fsDriver?: any;
|
||||
fsDriver?: FsDriver;
|
||||
extraRendererRules?: ExtraRendererRule[];
|
||||
customCss?: string;
|
||||
}
|
||||
@ -150,7 +127,7 @@ export interface RuleOptions {
|
||||
context: PluginContext;
|
||||
theme: any;
|
||||
postMessageSyntax: string;
|
||||
ResourceModel: any;
|
||||
ResourceModel: OptionsResourceModel;
|
||||
resourceBaseUrl: string;
|
||||
resources: any; // resourceId: Resource
|
||||
|
||||
@ -199,12 +176,12 @@ export interface RuleOptions {
|
||||
platformName?: string;
|
||||
}
|
||||
|
||||
export default class MdToHtml {
|
||||
export default class MdToHtml implements MarkupRenderer {
|
||||
|
||||
private resourceBaseUrl_: string;
|
||||
private ResourceModel_: any;
|
||||
private ResourceModel_: OptionsResourceModel;
|
||||
private contextCache_: any;
|
||||
private fsDriver_: any;
|
||||
private fsDriver_: FsDriver;
|
||||
|
||||
private cachedOutputs_: any = {};
|
||||
private lastCodeHighlightCacheKey_: string = null;
|
||||
|
@ -1,6 +1,6 @@
|
||||
|
||||
|
||||
import utils from '../utils';
|
||||
import * as utils from '../utils';
|
||||
|
||||
|
||||
export interface Options {
|
||||
|
@ -1,3 +1,4 @@
|
||||
import defaultResourceModel from '../defaultResourceModel';
|
||||
import linkReplacement from './linkReplacement';
|
||||
import { describe, test, expect } from '@jest/globals';
|
||||
|
||||
@ -24,7 +25,7 @@ describe('linkReplacement', () => {
|
||||
const resourceId = 'f6afba55bdf74568ac94f8d1e3578d2c';
|
||||
|
||||
const r = linkReplacement(`:/${resourceId}`, {
|
||||
ResourceModel: {},
|
||||
ResourceModel: defaultResourceModel,
|
||||
resources: {
|
||||
[resourceId]: {
|
||||
item: {},
|
||||
@ -42,7 +43,7 @@ describe('linkReplacement', () => {
|
||||
const resourceId = 'f6afba55bdf74568ac94f8d1e3578d2c';
|
||||
|
||||
const r = linkReplacement(`:/${resourceId}`, {
|
||||
ResourceModel: {},
|
||||
ResourceModel: defaultResourceModel,
|
||||
resources: {
|
||||
[resourceId]: {
|
||||
item: {},
|
||||
@ -62,7 +63,7 @@ describe('linkReplacement', () => {
|
||||
const resourceId = 'e6afba55bdf74568ac94f8d1e3578d2c';
|
||||
|
||||
const linkHtml = linkReplacement(`:/${resourceId}`, {
|
||||
ResourceModel: {},
|
||||
ResourceModel: defaultResourceModel,
|
||||
resources: {
|
||||
[resourceId]: {
|
||||
item: {},
|
||||
|
@ -1,4 +1,5 @@
|
||||
import utils, { ItemIdToUrlHandler } from '../utils';
|
||||
import { ItemIdToUrlHandler, OptionsResourceModel } from '../types';
|
||||
import * as utils from '../utils';
|
||||
import createEventHandlingAttrs from './createEventHandlingAttrs';
|
||||
const Entities = require('html-entities').AllHtmlEntities;
|
||||
const htmlentities = new Entities().encode;
|
||||
@ -8,7 +9,7 @@ const { getClassNameForMimeType } = require('font-awesome-filetypes');
|
||||
export interface Options {
|
||||
title?: string;
|
||||
resources?: any;
|
||||
ResourceModel?: any;
|
||||
ResourceModel?: OptionsResourceModel;
|
||||
linkRenderingType?: number;
|
||||
plainResourceRendering?: boolean;
|
||||
postMessageSyntax?: string;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Link } from '../MdToHtml';
|
||||
import { toForwardSlashes } from '../pathUtils';
|
||||
import { toForwardSlashes } from '@joplin/utils/path';
|
||||
import { LinkIndexes } from './rules/link_close';
|
||||
const Entities = require('html-entities').AllHtmlEntities;
|
||||
const htmlentities = new Entities().encode;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { RuleOptions } from '../../MdToHtml';
|
||||
import { attributesHtml } from '../../htmlUtils';
|
||||
import utils from '../../utils';
|
||||
import * as utils from '../../utils';
|
||||
|
||||
function renderImageHtml(before: string, src: string, after: string, ruleOptions: RuleOptions) {
|
||||
const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl, ruleOptions.itemIdToUrl);
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { RuleOptions } from '../../MdToHtml';
|
||||
import { attributesHtml } from '../../htmlUtils';
|
||||
import utils from '../../utils';
|
||||
import * as utils from '../../utils';
|
||||
import createEventHandlingAttrs from '../createEventHandlingAttrs';
|
||||
|
||||
function plugin(markdownIt: any, ruleOptions: RuleOptions) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { RuleOptions } from '../../MdToHtml';
|
||||
import linkReplacement from '../linkReplacement';
|
||||
import utils from '../../utils';
|
||||
import * as utils from '../../utils';
|
||||
|
||||
const urlUtils = require('../../urlUtils.js');
|
||||
|
||||
|
@ -1,9 +1,15 @@
|
||||
import { RenderResultPluginAsset } from './types';
|
||||
|
||||
interface Options {
|
||||
asHtml: boolean;
|
||||
}
|
||||
|
||||
// Utility function to convert the plugin assets to a list of LINK or SCRIPT tags
|
||||
// that can be included in the HEAD tag.
|
||||
function assetsToHeaders(pluginAssets, options = null) {
|
||||
const assetsToHeaders = (pluginAssets: RenderResultPluginAsset[], options: Options|null = null) => {
|
||||
options = { asHtml: false, ...options };
|
||||
|
||||
const headers = {};
|
||||
const headers: Record<string, string> = {};
|
||||
for (let i = 0; i < pluginAssets.length; i++) {
|
||||
const asset = pluginAssets[i];
|
||||
if (asset.mime === 'text/css') {
|
||||
@ -23,6 +29,6 @@ function assetsToHeaders(pluginAssets, options = null) {
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = assetsToHeaders;
|
||||
export default assetsToHeaders;
|
16
packages/renderer/defaultResourceModel.ts
Normal file
16
packages/renderer/defaultResourceModel.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import { OptionsResourceModel } from './types';
|
||||
|
||||
// Used for tests and when no ResourceModel is provided.
|
||||
|
||||
const defaultResourceModel: OptionsResourceModel = {
|
||||
isResourceUrl: (_url: string) => false,
|
||||
urlToId: _url => {
|
||||
throw new Error('Not implemented: urlToId');
|
||||
},
|
||||
filename: _url => {
|
||||
throw new Error('Not implemented: filename');
|
||||
},
|
||||
isSupportedImageMimeType: _type => false,
|
||||
};
|
||||
|
||||
export default defaultResourceModel;
|
@ -1,11 +1,11 @@
|
||||
import MarkupToHtml, { MarkupLanguage } from './MarkupToHtml';
|
||||
import MdToHtml from './MdToHtml';
|
||||
import HtmlToHtml from './HtmlToHtml';
|
||||
import utils from './utils';
|
||||
import * as utils from './utils';
|
||||
import setupLinkify from './MdToHtml/setupLinkify';
|
||||
import validateLinks from './MdToHtml/validateLinks';
|
||||
import headerAnchor from './headerAnchor';
|
||||
const assetsToHeaders = require('./assetsToHeaders');
|
||||
import assetsToHeaders from './assetsToHeaders';
|
||||
|
||||
export {
|
||||
MarkupToHtml,
|
||||
|
@ -1,34 +0,0 @@
|
||||
export function dirname(path: string) {
|
||||
if (!path) throw new Error('Path is empty');
|
||||
const s = path.split(/\/|\\/);
|
||||
s.pop();
|
||||
return s.join('/');
|
||||
}
|
||||
|
||||
export function basename(path: string) {
|
||||
if (!path) throw new Error('Path is empty');
|
||||
const s = path.split(/\/|\\/);
|
||||
return s[s.length - 1];
|
||||
}
|
||||
|
||||
export function filename(path: string, includeDir = false): string {
|
||||
if (!path) throw new Error('Path is empty');
|
||||
const output = includeDir ? path : basename(path);
|
||||
if (output.indexOf('.') < 0) return output;
|
||||
|
||||
const splitted = output.split('.');
|
||||
splitted.pop();
|
||||
return splitted.join('.');
|
||||
}
|
||||
|
||||
export function fileExtension(path: string) {
|
||||
if (!path) throw new Error('Path is empty');
|
||||
|
||||
const output = path.split('.');
|
||||
if (output.length <= 1) return '';
|
||||
return output[output.length - 1];
|
||||
}
|
||||
|
||||
export function toForwardSlashes(path: string) {
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
95
packages/renderer/types.ts
Normal file
95
packages/renderer/types.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { MarkupLanguage } from './MarkupToHtml';
|
||||
import { Options as NoteStyleOptions } from './noteStyle';
|
||||
|
||||
export type ItemIdToUrlHandler = (resource: any)=> string;
|
||||
|
||||
interface ResourceEntity {
|
||||
id: string;
|
||||
title?: string;
|
||||
mime?: string;
|
||||
file_extension?: string;
|
||||
}
|
||||
|
||||
export interface FsDriver {
|
||||
writeFile: (path: string, content: string, encoding: string)=> Promise<void>;
|
||||
exists: (path: string)=> Promise<boolean>;
|
||||
cacheCssToFile: (cssStrings: string[])=> Promise<any>;
|
||||
}
|
||||
|
||||
export interface RenderOptions {
|
||||
contentMaxWidth?: number;
|
||||
bodyOnly?: boolean;
|
||||
splitted?: boolean;
|
||||
enableLongPress?: boolean;
|
||||
postMessageSyntax?: string;
|
||||
|
||||
externalAssetsOnly?: boolean;
|
||||
highlightedKeywords?: string[];
|
||||
codeTheme?: string;
|
||||
theme?: any;
|
||||
|
||||
plugins?: Record<string, any>;
|
||||
audioPlayerEnabled?: boolean;
|
||||
videoPlayerEnabled?: boolean;
|
||||
pdfViewerEnabled?: boolean;
|
||||
|
||||
codeHighlightCacheKey?: string;
|
||||
plainResourceRendering?: boolean;
|
||||
|
||||
mapsToLine?: boolean;
|
||||
useCustomPdfViewer?: boolean;
|
||||
noteId?: string;
|
||||
vendorDir?: string;
|
||||
itemIdToUrl?: ItemIdToUrlHandler;
|
||||
allowedFilePrefixes?: string[];
|
||||
settingValue?: (pluginId: string, key: string)=> any;
|
||||
|
||||
resources?: Record<string, ResourceEntity>;
|
||||
|
||||
// HtmlToHtml only
|
||||
whiteBackgroundNoteRendering?: boolean;
|
||||
}
|
||||
|
||||
export interface RenderResultPluginAsset {
|
||||
name: string;
|
||||
mime: string;
|
||||
path: string;
|
||||
|
||||
// For built-in Mardown-it plugins, the asset path is relative (and can be
|
||||
// found inside the @joplin/renderer package), while for external plugins
|
||||
// (content scripts), the path is absolute. We use this property to tell if
|
||||
// it's relative or absolute, as that will inform how it's loaded in various
|
||||
// places.
|
||||
pathIsAbsolute: boolean;
|
||||
}
|
||||
|
||||
export interface RenderResult {
|
||||
html: string;
|
||||
pluginAssets: RenderResultPluginAsset[];
|
||||
cssStrings: string[];
|
||||
}
|
||||
|
||||
export interface MarkupRenderer {
|
||||
render(markup: string, theme: any, options: RenderOptions): Promise<RenderResult>;
|
||||
clearCache(): void;
|
||||
allAssets(theme: any, noteStyleOptions: NoteStyleOptions|null): Promise<RenderResultPluginAsset[]>;
|
||||
}
|
||||
|
||||
interface StripMarkupOptions {
|
||||
collapseWhiteSpaces: boolean;
|
||||
}
|
||||
|
||||
export interface MarkupToHtmlConverter {
|
||||
render(markupLanguage: MarkupLanguage, markup: string, theme: any, options: any): Promise<RenderResult>;
|
||||
clearCache(markupLanguage: MarkupLanguage): void;
|
||||
stripMarkup(markupLanguage: MarkupLanguage, markup: string, options: StripMarkupOptions): string;
|
||||
allAssets(markupLanguage: MarkupLanguage, theme: any, noteStyleOptions: NoteStyleOptions|null): Promise<RenderResultPluginAsset[]>;
|
||||
}
|
||||
|
||||
export interface OptionsResourceModel {
|
||||
isResourceUrl: (url: string)=> boolean;
|
||||
urlToId: (url: string)=> string;
|
||||
filename: (resource: ResourceEntity, encryptedBlob?: boolean)=> string;
|
||||
isSupportedImageMimeType: (type: string)=> boolean;
|
||||
fullPath?: (resource: ResourceEntity, encryptedBlob?: boolean)=> string;
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { ItemIdToUrlHandler, OptionsResourceModel } from './types';
|
||||
|
||||
const Entities = require('html-entities').AllHtmlEntities;
|
||||
const htmlentities = new Entities().encode;
|
||||
|
||||
@ -9,16 +11,14 @@ const FetchStatuses = {
|
||||
FETCH_STATUS_ERROR: 3,
|
||||
};
|
||||
|
||||
const utils: any = {};
|
||||
|
||||
utils.getAttr = function(attrs: string[], name: string, defaultValue: string = null) {
|
||||
export const getAttr = function(attrs: string[], name: string, defaultValue: string = null) {
|
||||
for (let i = 0; i < attrs.length; i++) {
|
||||
if (attrs[i][0] === name) return attrs[i].length > 1 ? attrs[i][1] : null;
|
||||
}
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
utils.notDownloadedResource = function() {
|
||||
export const notDownloadedResource = function() {
|
||||
return `
|
||||
<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1280 1344c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm256 0c0-35-29-64-64-64s-64 29-64 64 29 64 64 64 64-29 64-64zm128-224v320c0 53-43 96-96 96H96c-53 0-96-43-96-96v-320c0-53 43-96 96-96h465l135 136c37 36 85 56 136 56s99-20 136-56l136-136h464c53 0 96 43 96 96zm-325-569c10 24 5 52-14 70l-448 448c-12 13-29 19-45 19s-33-6-45-19L339 621c-19-18-24-46-14-70 10-23 33-39 59-39h256V64c0-35 29-64 64-64h256c35 0 64 29 64 64v448h256c26 0 49 16 59 39z"/>
|
||||
@ -26,7 +26,7 @@ utils.notDownloadedResource = function() {
|
||||
`;
|
||||
};
|
||||
|
||||
utils.notDownloadedImage = function() {
|
||||
export const notDownloadedImage = function() {
|
||||
// https://github.com/ForkAwesome/Fork-Awesome/blob/master/src/icons/svg/file-image-o.svg
|
||||
// Height changed to 1795
|
||||
return `
|
||||
@ -36,7 +36,7 @@ utils.notDownloadedImage = function() {
|
||||
`;
|
||||
};
|
||||
|
||||
utils.notDownloadedFile = function() {
|
||||
export const notDownloadedFile = function() {
|
||||
// https://github.com/ForkAwesome/Fork-Awesome/blob/master/src/icons/svg/file-o.svg
|
||||
return `
|
||||
<svg width="1925" height="1792" xmlns="http://www.w3.org/2000/svg">
|
||||
@ -45,7 +45,7 @@ utils.notDownloadedFile = function() {
|
||||
`;
|
||||
};
|
||||
|
||||
utils.errorImage = function() {
|
||||
export const errorImage = function() {
|
||||
// https://github.com/ForkAwesome/Fork-Awesome/blob/master/src/icons/svg/times-circle.svg
|
||||
return `
|
||||
<svg width="1795" height="1795" xmlns="http://www.w3.org/2000/svg">
|
||||
@ -54,7 +54,7 @@ utils.errorImage = function() {
|
||||
`;
|
||||
};
|
||||
|
||||
utils.loaderImage = function() {
|
||||
export const loaderImage = function() {
|
||||
// https://github.com/ForkAwesome/Fork-Awesome/blob/master/src/icons/svg/hourglass-half.svg
|
||||
return `
|
||||
<svg width="1536" height="1790" xmlns="http://www.w3.org/2000/svg">
|
||||
@ -63,21 +63,21 @@ utils.loaderImage = function() {
|
||||
`;
|
||||
};
|
||||
|
||||
utils.resourceStatusImage = function(status: string) {
|
||||
if (status === 'notDownloaded') return utils.notDownloadedResource();
|
||||
return utils.resourceStatusFile(status);
|
||||
export const resourceStatusImage = function(status: string) {
|
||||
if (status === 'notDownloaded') return notDownloadedResource();
|
||||
return resourceStatusFile(status);
|
||||
};
|
||||
|
||||
utils.resourceStatusFile = function(status: string) {
|
||||
if (status === 'notDownloaded') return utils.notDownloadedResource();
|
||||
if (status === 'downloading') return utils.loaderImage();
|
||||
if (status === 'encrypted') return utils.loaderImage();
|
||||
if (status === 'error') return utils.errorImage();
|
||||
export const resourceStatusFile = function(status: string) {
|
||||
if (status === 'notDownloaded') return notDownloadedResource();
|
||||
if (status === 'downloading') return loaderImage();
|
||||
if (status === 'encrypted') return loaderImage();
|
||||
if (status === 'error') return errorImage();
|
||||
|
||||
throw new Error(`Unknown status: ${status}`);
|
||||
};
|
||||
|
||||
utils.resourceStatusIndex = function(status: string) {
|
||||
export const resourceStatusIndex = function(status: string) {
|
||||
if (status === 'error') return -1;
|
||||
if (status === 'notDownloaded') return 0;
|
||||
if (status === 'downloading') return 1;
|
||||
@ -87,7 +87,7 @@ utils.resourceStatusIndex = function(status: string) {
|
||||
throw new Error(`Unknown status: ${status}`);
|
||||
};
|
||||
|
||||
utils.resourceStatusName = function(index: number) {
|
||||
export const resourceStatusName = function(index: number) {
|
||||
if (index === -1) return 'error';
|
||||
if (index === 0) return 'notDownloaded';
|
||||
if (index === 1) return 'downloading';
|
||||
@ -97,34 +97,32 @@ utils.resourceStatusName = function(index: number) {
|
||||
throw new Error(`Unknown index: ${index}`);
|
||||
};
|
||||
|
||||
utils.resourceStatus = function(ResourceModel: any, resourceInfo: any) {
|
||||
export const resourceStatus = function(ResourceModel: OptionsResourceModel, resourceInfo: any) {
|
||||
if (!ResourceModel) return 'ready';
|
||||
|
||||
let resourceStatus = 'ready';
|
||||
let status = 'ready';
|
||||
|
||||
if (resourceInfo) {
|
||||
const resource = resourceInfo.item;
|
||||
const localState = resourceInfo.localState;
|
||||
|
||||
if (localState.fetch_status === FetchStatuses.FETCH_STATUS_IDLE) {
|
||||
resourceStatus = 'notDownloaded';
|
||||
status = 'notDownloaded';
|
||||
} else if (localState.fetch_status === FetchStatuses.FETCH_STATUS_STARTED) {
|
||||
resourceStatus = 'downloading';
|
||||
status = 'downloading';
|
||||
} else if (localState.fetch_status === FetchStatuses.FETCH_STATUS_DONE) {
|
||||
if (resource.encryption_blob_encrypted || resource.encryption_applied) {
|
||||
resourceStatus = 'encrypted';
|
||||
status = 'encrypted';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resourceStatus = 'notDownloaded';
|
||||
status = 'notDownloaded';
|
||||
}
|
||||
|
||||
return resourceStatus;
|
||||
return status;
|
||||
};
|
||||
|
||||
export type ItemIdToUrlHandler = (resource: any)=> string;
|
||||
|
||||
utils.imageReplacement = function(ResourceModel: any, src: string, resources: any, resourceBaseUrl: string, itemIdToUrl: ItemIdToUrlHandler = null) {
|
||||
export const imageReplacement = function(ResourceModel: OptionsResourceModel, src: string, resources: any, resourceBaseUrl: string, itemIdToUrl: ItemIdToUrlHandler = null) {
|
||||
if (!ResourceModel || !resources) return null;
|
||||
|
||||
if (!ResourceModel.isResourceUrl(src)) return null;
|
||||
@ -132,11 +130,11 @@ utils.imageReplacement = function(ResourceModel: any, src: string, resources: an
|
||||
const resourceId = ResourceModel.urlToId(src);
|
||||
const result = resources[resourceId];
|
||||
const resource = result ? result.item : null;
|
||||
const resourceStatus = utils.resourceStatus(ResourceModel, result);
|
||||
const status = resourceStatus(ResourceModel, result);
|
||||
|
||||
if (resourceStatus !== 'ready') {
|
||||
const icon = utils.resourceStatusImage(resourceStatus);
|
||||
return `<div class="not-loaded-resource resource-status-${resourceStatus}" data-resource-id="${resourceId}">` + `<img src="data:image/svg+xml;utf8,${htmlentities(icon)}"/>` + '</div>';
|
||||
if (status !== 'ready') {
|
||||
const icon = resourceStatusImage(status);
|
||||
return `<div class="not-loaded-resource resource-status-${status}" data-resource-id="${resourceId}">` + `<img src="data:image/svg+xml;utf8,${htmlentities(icon)}"/>` + '</div>';
|
||||
}
|
||||
const mime = resource.mime ? resource.mime.toLowerCase() : '';
|
||||
if (ResourceModel.isSupportedImageMimeType(mime)) {
|
||||
@ -172,6 +170,4 @@ utils.imageReplacement = function(ResourceModel: any, src: string, resources: an
|
||||
|
||||
// Used in mobile app when enableLongPress = true. Tells for how long
|
||||
// the resource should be pressed before the menu is shown.
|
||||
utils.longPressDelay = 500;
|
||||
|
||||
export default utils;
|
||||
export const longPressDelay = 500;
|
||||
|
@ -16,7 +16,7 @@ import { NoteEntity } from '@joplin/lib/services/database/types';
|
||||
import { formatDateTime } from './time';
|
||||
import { ErrorBadRequest, ErrorForbidden, ErrorNotFound } from './errors';
|
||||
import { MarkupToHtml } from '@joplin/renderer';
|
||||
import { OptionsResourceModel } from '@joplin/renderer/MarkupToHtml';
|
||||
import { OptionsResourceModel } from '@joplin/renderer/types';
|
||||
import { isValidHeaderIdentifier } from '@joplin/lib/services/e2ee/EncryptionService';
|
||||
const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js');
|
||||
import { themeStyle } from '@joplin/lib/theme';
|
||||
|
@ -13,7 +13,8 @@
|
||||
"./net": "./dist/net.js",
|
||||
"./time": "./dist/time.js",
|
||||
"./types": "./dist/types.js",
|
||||
"./url": "./dist/url.js"
|
||||
"./url": "./dist/url.js",
|
||||
"./path": "./dist/path.js"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
58
packages/utils/path.test.ts
Normal file
58
packages/utils/path.test.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import { extractExecutablePath, quotePath, toFileProtocolPath, unquotePath } from './path';
|
||||
|
||||
describe('path', () => {
|
||||
it('should quote and unquote paths', (async () => {
|
||||
const testCases = [
|
||||
['', ''],
|
||||
['/my/path', '/my/path'],
|
||||
['/my/path with spaces', '"/my/path with spaces"'],
|
||||
['/my/weird"path', '"/my/weird\\"path"'],
|
||||
['c:\\Windows\\test.dll', 'c:\\Windows\\test.dll'],
|
||||
['c:\\Windows\\test test.dll', '"c:\\Windows\\test test.dll"'],
|
||||
];
|
||||
|
||||
for (let i = 0; i < testCases.length; i++) {
|
||||
const t = testCases[i];
|
||||
expect(quotePath(t[0])).toBe(t[1]);
|
||||
expect(unquotePath(quotePath(t[0]))).toBe(t[0]);
|
||||
}
|
||||
}));
|
||||
|
||||
it('should extract executable path from command', (async () => {
|
||||
const testCases = [
|
||||
['', ''],
|
||||
['/my/cmd -some -args', '/my/cmd'],
|
||||
['"/my/cmd" -some -args', '"/my/cmd"'],
|
||||
['"/my/cmd"', '"/my/cmd"'],
|
||||
['"/my/cmd and space" -some -flags', '"/my/cmd and space"'],
|
||||
['"" -some -flags', '""'],
|
||||
];
|
||||
|
||||
for (let i = 0; i < testCases.length; i++) {
|
||||
const t = testCases[i];
|
||||
expect(extractExecutablePath(t[0])).toBe(t[1]);
|
||||
}
|
||||
}));
|
||||
|
||||
it('should create correct fileURL syntax', (async () => {
|
||||
const testCases_win32 = [
|
||||
['C:\\handle\\space test', 'file:///C:/handle/space%20test'],
|
||||
['C:\\escapeplus\\+', 'file:///C:/escapeplus/%2B'],
|
||||
['C:\\handle\\single quote\'', 'file:///C:/handle/single%20quote%27'],
|
||||
];
|
||||
const testCases_unixlike = [
|
||||
['/handle/space test', 'file:///handle/space%20test'],
|
||||
['/escapeplus/+', 'file:///escapeplus/%2B'],
|
||||
['/handle/single quote\'', 'file:///handle/single%20quote%27'],
|
||||
];
|
||||
|
||||
for (let i = 0; i < testCases_win32.length; i++) {
|
||||
const t = testCases_win32[i];
|
||||
expect(toFileProtocolPath(t[0], 'win32')).toBe(t[1]);
|
||||
}
|
||||
for (let i = 0; i < testCases_unixlike.length; i++) {
|
||||
const t = testCases_unixlike[i];
|
||||
expect(toFileProtocolPath(t[0], 'linux')).toBe(t[1]);
|
||||
}
|
||||
}));
|
||||
});
|
134
packages/utils/path.ts
Normal file
134
packages/utils/path.ts
Normal file
@ -0,0 +1,134 @@
|
||||
/* eslint no-useless-escape: 0*/
|
||||
|
||||
export function dirname(path: string) {
|
||||
if (!path) throw new Error('Path is empty');
|
||||
const s = path.split(/\/|\\/);
|
||||
s.pop();
|
||||
return s.join('/');
|
||||
}
|
||||
|
||||
export function basename(path: string) {
|
||||
if (!path) throw new Error('Path is empty');
|
||||
const s = path.split(/\/|\\/);
|
||||
return s[s.length - 1];
|
||||
}
|
||||
|
||||
export function filename(path: string, includeDir = false) {
|
||||
if (!path) throw new Error('Path is empty');
|
||||
const output = includeDir ? path : basename(path);
|
||||
if (output.indexOf('.') < 0) return output;
|
||||
|
||||
const splitted = output.split('.');
|
||||
splitted.pop();
|
||||
return splitted.join('.');
|
||||
}
|
||||
|
||||
export function fileExtension(path: string) {
|
||||
if (!path) throw new Error('Path is empty');
|
||||
|
||||
const output = path.split('.');
|
||||
if (output.length <= 1) return '';
|
||||
return output[output.length - 1];
|
||||
}
|
||||
|
||||
export function isHidden(path: string) {
|
||||
const b = basename(path);
|
||||
if (!b.length) throw new Error(`Path empty or not a valid path: ${path}`);
|
||||
return b[0] === '.';
|
||||
}
|
||||
|
||||
// Note that this function only sanitizes a file extension - it does NOT extract
|
||||
// the file extension from a filename. So the way you'd normally call this is
|
||||
// `safeFileExtension(fileExtension(filename))`
|
||||
export function safeFileExtension(e: string, maxLength: number|null = null) {
|
||||
// In theory the file extension can have any length but in practice Joplin
|
||||
// expects a fixed length, so we limit it to 20 which should cover most cases.
|
||||
// Note that it means that a file extension longer than 20 will break
|
||||
// external editing (since the extension would be truncated).
|
||||
// https://discourse.joplinapp.org/t/troubles-with-webarchive-files-on-ios/10447
|
||||
if (maxLength === null) maxLength = 20;
|
||||
if (!e || !e.replace) return '';
|
||||
return e.replace(/[^a-zA-Z0-9]/g, '').substring(0, maxLength);
|
||||
}
|
||||
|
||||
export function safeFilename(e: string, maxLength: number|null = null, allowSpaces = false) {
|
||||
if (maxLength === null) maxLength = 32;
|
||||
if (!e || !e.replace) return '';
|
||||
const regex = allowSpaces ? /[^a-zA-Z0-9\-_\(\)\. ]/g : /[^a-zA-Z0-9\-_\(\)\.]/g;
|
||||
const output = e.replace(regex, '_');
|
||||
return output.substring(0, maxLength);
|
||||
}
|
||||
|
||||
export function toFileProtocolPath(filePathEncode: string, os: string|null = null) {
|
||||
if (os === null) os = process.platform;
|
||||
|
||||
if (os === 'win32') {
|
||||
filePathEncode = filePathEncode.replace(/\\/g, '/'); // replace backslash in windows pathname with slash e.g. c:\temp to c:/temp
|
||||
filePathEncode = `/${filePathEncode}`; // put slash in front of path to comply with windows fileURL syntax
|
||||
}
|
||||
|
||||
filePathEncode = encodeURI(filePathEncode);
|
||||
filePathEncode = filePathEncode.replace(/\+/g, '%2B'); // escape '+' with unicode
|
||||
return `file://${filePathEncode.replace(/\'/g, '%27')}`; // escape '(single quote) with unicode, to prevent crashing the html view
|
||||
}
|
||||
|
||||
export function toSystemSlashes(path: string, os: string|null = null) {
|
||||
if (os === null) os = process.platform;
|
||||
if (os === 'win32') return path.replace(/\//g, '\\');
|
||||
return path.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
export function toForwardSlashes(path: string) {
|
||||
return toSystemSlashes(path, 'linux');
|
||||
}
|
||||
|
||||
export function rtrimSlashes(path: string) {
|
||||
return path.replace(/[\/\\]+$/, '');
|
||||
}
|
||||
|
||||
export function ltrimSlashes(path: string) {
|
||||
return path.replace(/^\/+/, '');
|
||||
}
|
||||
|
||||
export function trimSlashes(path: string): string {
|
||||
return ltrimSlashes(rtrimSlashes(path));
|
||||
}
|
||||
|
||||
export function quotePath(path: string) {
|
||||
if (!path) return '';
|
||||
if (path.indexOf('"') < 0 && path.indexOf(' ') < 0) return path;
|
||||
path = path.replace(/"/, '\\"');
|
||||
return `"${path}"`;
|
||||
}
|
||||
|
||||
export function unquotePath(path: string) {
|
||||
if (!path.length) return '';
|
||||
if (path.length && path[0] === '"') {
|
||||
path = path.substring(1, path.length - 1);
|
||||
}
|
||||
path = path.replace(/\\"/, '"');
|
||||
return path;
|
||||
}
|
||||
|
||||
export function extractExecutablePath(cmd: string) {
|
||||
if (!cmd.length) return '';
|
||||
|
||||
const quoteType = ['"', '\''].indexOf(cmd[0]) >= 0 ? cmd[0] : '';
|
||||
|
||||
let output = '';
|
||||
for (let i = 0; i < cmd.length; i++) {
|
||||
const c = cmd[i];
|
||||
if (quoteType) {
|
||||
if (i > 0 && c === quoteType) {
|
||||
output += c;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (c === ' ') break;
|
||||
}
|
||||
|
||||
output += c;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
Loading…
Reference in New Issue
Block a user