1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-11-27 08:21:03 +02:00

Chore: Refactor renderer package: Limit dependency on @joplin/lib and improve type safety (#9701)

This commit is contained in:
Henry Heino 2024-01-18 03:20:10 -08:00 committed by GitHub
parent 352ee6496e
commit f5e1e45f6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 457 additions and 393 deletions

View File

@ -734,6 +734,7 @@ packages/lib/models/utils/itemCanBeEncrypted.js
packages/lib/models/utils/paginatedFeed.js packages/lib/models/utils/paginatedFeed.js
packages/lib/models/utils/paginationToSql.js packages/lib/models/utils/paginationToSql.js
packages/lib/models/utils/readOnly.js packages/lib/models/utils/readOnly.js
packages/lib/models/utils/resourceUtils.js
packages/lib/models/utils/types.js packages/lib/models/utils/types.js
packages/lib/models/utils/userData.test.js packages/lib/models/utils/userData.test.js
packages/lib/models/utils/userData.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/rules/source_map.js
packages/renderer/MdToHtml/setupLinkify.js packages/renderer/MdToHtml/setupLinkify.js
packages/renderer/MdToHtml/validateLinks.js packages/renderer/MdToHtml/validateLinks.js
packages/renderer/assetsToHeaders.js
packages/renderer/defaultResourceModel.js
packages/renderer/headerAnchor.js packages/renderer/headerAnchor.js
packages/renderer/highlight.js packages/renderer/highlight.js
packages/renderer/htmlUtils.test.js packages/renderer/htmlUtils.test.js
packages/renderer/htmlUtils.js packages/renderer/htmlUtils.js
packages/renderer/index.js packages/renderer/index.js
packages/renderer/noteStyle.js packages/renderer/noteStyle.js
packages/renderer/pathUtils.js packages/renderer/types.js
packages/renderer/utils.js packages/renderer/utils.js
packages/tools/build-release-stats.test.js packages/tools/build-release-stats.test.js
packages/tools/build-release-stats.js packages/tools/build-release-stats.js

5
.gitignore vendored
View File

@ -714,6 +714,7 @@ packages/lib/models/utils/itemCanBeEncrypted.js
packages/lib/models/utils/paginatedFeed.js packages/lib/models/utils/paginatedFeed.js
packages/lib/models/utils/paginationToSql.js packages/lib/models/utils/paginationToSql.js
packages/lib/models/utils/readOnly.js packages/lib/models/utils/readOnly.js
packages/lib/models/utils/resourceUtils.js
packages/lib/models/utils/types.js packages/lib/models/utils/types.js
packages/lib/models/utils/userData.test.js packages/lib/models/utils/userData.test.js
packages/lib/models/utils/userData.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/rules/source_map.js
packages/renderer/MdToHtml/setupLinkify.js packages/renderer/MdToHtml/setupLinkify.js
packages/renderer/MdToHtml/validateLinks.js packages/renderer/MdToHtml/validateLinks.js
packages/renderer/assetsToHeaders.js
packages/renderer/defaultResourceModel.js
packages/renderer/headerAnchor.js packages/renderer/headerAnchor.js
packages/renderer/highlight.js packages/renderer/highlight.js
packages/renderer/htmlUtils.test.js packages/renderer/htmlUtils.test.js
packages/renderer/htmlUtils.js packages/renderer/htmlUtils.js
packages/renderer/index.js packages/renderer/index.js
packages/renderer/noteStyle.js packages/renderer/noteStyle.js
packages/renderer/pathUtils.js packages/renderer/types.js
packages/renderer/utils.js packages/renderer/utils.js
packages/tools/build-release-stats.test.js packages/tools/build-release-stats.test.js
packages/tools/build-release-stats.js packages/tools/build-release-stats.js

View File

@ -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', () => { describe('MarkupToHtml', () => {

View File

@ -10,7 +10,7 @@ import EncryptionConfigScreen from '../EncryptionConfigScreen/EncryptionConfigSc
import { reg } from '@joplin/lib/registry'; import { reg } from '@joplin/lib/registry';
const { connect } = require('react-redux'); const { connect } = require('react-redux');
const { themeStyle } = require('@joplin/lib/theme'); 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 SyncTargetRegistry from '@joplin/lib/SyncTargetRegistry';
import * as shared from '@joplin/lib/components/shared/config/config-shared.js'; import * as shared from '@joplin/lib/components/shared/config/config-shared.js';
import ClipperConfigScreen from '../ClipperConfigScreen'; import ClipperConfigScreen from '../ClipperConfigScreen';

View File

@ -2,7 +2,7 @@ import AsyncActionQueue from '@joplin/lib/AsyncActionQueue';
import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils'; import { ToolbarButtonInfo } from '@joplin/lib/services/commands/ToolbarButtonUtils';
import { PluginStates } from '@joplin/lib/services/plugins/reducer'; import { PluginStates } from '@joplin/lib/services/plugins/reducer';
import { MarkupLanguage } from '@joplin/renderer'; import { MarkupLanguage } from '@joplin/renderer';
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/MarkupToHtml'; import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/types';
import { MarkupToHtmlOptions } from './useMarkupToHtml'; import { MarkupToHtmlOptions } from './useMarkupToHtml';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { ProcessResultsRow } from '@joplin/lib/services/search/SearchEngine'; import { ProcessResultsRow } from '@joplin/lib/services/search/SearchEngine';

View File

@ -5,7 +5,7 @@ const { themeStyle } = require('../../global-style.js');
import markupLanguageUtils from '@joplin/lib/markupLanguageUtils'; import markupLanguageUtils from '@joplin/lib/markupLanguageUtils';
import useEditPopup from './useEditPopup'; import useEditPopup from './useEditPopup';
import Logger from '@joplin/utils/Logger'; import Logger from '@joplin/utils/Logger';
const { assetsToHeaders } = require('@joplin/renderer'); import { assetsToHeaders } from '@joplin/renderer';
const logger = Logger.create('NoteBodyViewer/useSource'); const logger = Logger.create('NoteBodyViewer/useSource');

View File

@ -7,9 +7,9 @@ import markdownUtils from '../markdownUtils';
import { _ } from '../locale'; import { _ } from '../locale';
import { ResourceEntity, ResourceLocalStateEntity, ResourceOcrStatus, SqlQuery } from '../services/database/types'; import { ResourceEntity, ResourceLocalStateEntity, ResourceOcrStatus, SqlQuery } from '../services/database/types';
import ResourceLocalState from './ResourceLocalState'; 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 { mime } = require('../mime-utils.js');
const { filename, safeFilename } = require('../path-utils');
const { FsDriverDummy } = require('../fs-driver-dummy.js'); const { FsDriverDummy } = require('../fs-driver-dummy.js');
import JoplinError from '../JoplinError'; import JoplinError from '../JoplinError';
import itemCanBeEncrypted from './utils/itemCanBeEncrypted'; import itemCanBeEncrypted from './utils/itemCanBeEncrypted';
@ -23,6 +23,7 @@ import { RecognizeResultLine } from '../services/ocr/utils/types';
import eventManager, { EventName } from '../eventManager'; import eventManager, { EventName } from '../eventManager';
import { unique } from '../array'; import { unique } from '../array';
import isSqliteSyntaxError from '../services/database/isSqliteSyntaxError'; import isSqliteSyntaxError from '../services/database/isSqliteSyntaxError';
import { internalUrl, isResourceUrl, isSupportedImageMimeType, resourceFilename, resourceFullPath, resourcePathToId, resourceRelativePath, resourceUrlToId } from './utils/resourceUtils';
export default class Resource extends BaseItem { export default class Resource extends BaseItem {
@ -56,8 +57,7 @@ export default class Resource extends BaseItem {
} }
public static isSupportedImageMimeType(type: string) { public static isSupportedImageMimeType(type: string) {
const imageMimeTypes = ['image/jpg', 'image/jpeg', 'image/png', 'image/gif', 'image/svg+xml', 'image/webp', 'image/avif']; return isSupportedImageMimeType(type);
return imageMimeTypes.indexOf(type.toLowerCase()) >= 0;
} }
public static fetchStatuses(resourceIds: string[]): Promise<any[]> { public static fetchStatuses(resourceIds: string[]): Promise<any[]> {
@ -121,10 +121,7 @@ export default class Resource extends BaseItem {
} }
public static filename(resource: ResourceEntity, encryptedBlob = false) { public static filename(resource: ResourceEntity, encryptedBlob = false) {
let extension = encryptedBlob ? 'crypted' : resource.file_extension; return resourceFilename(resource, encryptedBlob);
if (!extension) extension = resource.mime ? mime.toFileExtension(resource.mime) : '';
extension = extension ? `.${extension}` : '';
return resource.id + extension;
} }
public static friendlySafeFilename(resource: ResourceEntity) { public static friendlySafeFilename(resource: ResourceEntity) {
@ -137,11 +134,11 @@ export default class Resource extends BaseItem {
} }
public static relativePath(resource: ResourceEntity, encryptedBlob = false) { 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) { 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) { public static async isReady(resource: ResourceEntity) {
@ -270,11 +267,11 @@ export default class Resource extends BaseItem {
} }
public static internalUrl(resource: ResourceEntity) { public static internalUrl(resource: ResourceEntity) {
return `:/${resource.id}`; return internalUrl(resource);
} }
public static pathToId(path: string) { public static pathToId(path: string) {
return filename(path); return resourcePathToId(path);
} }
public static async content(resource: ResourceEntity) { public static async content(resource: ResourceEntity) {
@ -282,12 +279,11 @@ export default class Resource extends BaseItem {
} }
public static isResourceUrl(url: string) { public static isResourceUrl(url: string) {
return url && url.length === 34 && url[0] === ':' && url[1] === '/'; return isResourceUrl(url);
} }
public static urlToId(url: string) { public static urlToId(url: string) {
if (!this.isResourceUrl(url)) throw new Error(`Not a valid resource URL: ${url}`); return resourceUrlToId(url);
return url.substr(2);
} }
public static async localState(resourceOrId: any): Promise<ResourceLocalStateEntity> { public static async localState(resourceOrId: any): Promise<ResourceLocalStateEntity> {

View 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;
};

View File

@ -1,65 +1,8 @@
/* eslint no-useless-escape: 0*/ /* eslint no-useless-escape: 0*/
const { _ } = require('./locale'); import { _ } from './locale';
import { fileExtension, filename, safeFileExtension } from '@joplin/utils/path';
export function dirname(path: string) { export * from '@joplin/utils/path';
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);
}
let friendlySafeFilename_blackListChars = '/\n\r<>:\'"\\|?*#'; let friendlySafeFilename_blackListChars = '/\n\r<>:\'"\\|?*#';
for (let i = 0; i < 32; i++) { 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; 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;
}

View File

@ -1,9 +1,7 @@
const { extractExecutablePath, quotePath, unquotePath, friendlySafeFilename, toFileProtocolPath } = require('./path-utils'); const { friendlySafeFilename } = require('./path-utils');
describe('pathUtils', () => { describe('pathUtils', () => {
it('should create friendly safe filename', (async () => { it('should create friendly safe filename', (async () => {
const testCases = [ const testCases = [
['生活', '生活'], ['生活', '生活'],
@ -32,59 +30,4 @@ describe('pathUtils', () => {
expect(friendlySafeFilename('thatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylongthatsreallylong.md', null, true)).toBe('thatsreallylongthatsreallylongthatsreallylongthats.md'); 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]);
}
}));
}); });

View File

@ -12,7 +12,7 @@ import { basename, friendlySafeFilename, rtrimSlashes, dirname } from '../../pat
import htmlpack from '@joplin/htmlpack'; import htmlpack from '@joplin/htmlpack';
const { themeStyle } = require('../../theme'); const { themeStyle } = require('../../theme');
const { escapeHtml } = require('../../string-utils.js'); 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 { export default class InteropService_Exporter_Html extends InteropService_Exporter_Base {

View File

@ -1,6 +1,6 @@
import { PluginStates } from '../reducer'; import { PluginStates } from '../reducer';
import { ContentScriptType, ContentScriptContext, PostMessageHandler, ContentScriptModule } from '../api/types'; 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 shim from '../../../shim';
import Logger from '@joplin/utils/Logger'; import Logger from '@joplin/utils/Logger';
import PluginService from '../PluginService'; import PluginService from '../PluginService';

View File

@ -1,10 +1,10 @@
import htmlUtils from './htmlUtils'; import htmlUtils from './htmlUtils';
import linkReplacement from './MdToHtml/linkReplacement'; import linkReplacement from './MdToHtml/linkReplacement';
import utils, { ItemIdToUrlHandler } from './utils'; import * as utils from './utils';
import InMemoryCache from './InMemoryCache'; import InMemoryCache from './InMemoryCache';
import { RenderResult } from './MarkupToHtml';
import noteStyle, { whiteBackgroundNoteStyle } from './noteStyle'; import noteStyle, { whiteBackgroundNoteStyle } from './noteStyle';
import { Options as NoteStyleOptions } from './noteStyle'; import { Options as NoteStyleOptions } from './noteStyle';
import { FsDriver, MarkupRenderer, OptionsResourceModel, RenderOptions, RenderResult } from './types';
const md5 = require('md5'); const md5 = require('md5');
// Renderered notes can potentially be quite large (for example // Renderered notes can potentially be quite large (for example
@ -17,38 +17,12 @@ export interface SplittedHtml {
css: string; 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 { interface Options {
ResourceModel: any; ResourceModel: OptionsResourceModel;
resourceBaseUrl?: string; resourceBaseUrl?: string;
fsDriver?: FsDriver; 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 // https://github.com/es-shims/String.prototype.trimStart/blob/main/implementation.js
function trimStart(s: string): string { function trimStart(s: string): string {
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
@ -56,12 +30,12 @@ function trimStart(s: string): string {
return s.replace(startWhitespace, ''); return s.replace(startWhitespace, '');
} }
export default class HtmlToHtml { export default class HtmlToHtml implements MarkupRenderer {
private resourceBaseUrl_; private resourceBaseUrl_;
private ResourceModel_; private ResourceModel_;
private cache_; private cache_;
private fsDriver_: any; private fsDriver_: FsDriver;
public constructor(options: Options = null) { public constructor(options: Options = null) {
options = { options = {
@ -101,6 +75,10 @@ export default class HtmlToHtml {
return [await this.fsDriver().cacheCssToFile(cssStrings)]; 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 // Note: the "theme" variable is ignored and instead the light theme is
// always used for HTML notes. // always used for HTML notes.
// See: https://github.com/laurent22/joplin/issues/3698 // See: https://github.com/laurent22/joplin/issues/3698

View File

@ -3,6 +3,8 @@ import HtmlToHtml from './HtmlToHtml';
import htmlUtils from './htmlUtils'; import htmlUtils from './htmlUtils';
import { Options as NoteStyleOptions } from './noteStyle'; import { Options as NoteStyleOptions } from './noteStyle';
import { AllHtmlEntities } from 'html-entities'; import { AllHtmlEntities } from 'html-entities';
import { FsDriver, MarkupRenderer, MarkupToHtmlConverter, OptionsResourceModel, RenderResult } from './types';
import defaultResourceModel from './defaultResourceModel';
const MarkdownIt = require('markdown-it'); const MarkdownIt = require('markdown-it');
export enum MarkupLanguage { export enum MarkupLanguage {
@ -11,29 +13,6 @@ export enum MarkupLanguage {
Any = 3, 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 { export interface Options {
isSafeMode?: boolean; isSafeMode?: boolean;
ResourceModel?: OptionsResourceModel; ResourceModel?: OptionsResourceModel;
@ -42,23 +21,21 @@ export interface Options {
resourceBaseUrl?: string; resourceBaseUrl?: string;
pluginOptions?: any; // Not sure if needed pluginOptions?: any; // Not sure if needed
tempDir?: string; // 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_MARKDOWN: number = MarkupLanguage.Markdown;
public static MARKUP_LANGUAGE_HTML: number = MarkupLanguage.Html; public static MARKUP_LANGUAGE_HTML: number = MarkupLanguage.Html;
private renderers_: any = {}; private renderers_: Record<string, MarkupRenderer> = {};
private options_: Options; private options_: Options;
private rawMarkdownIt_: any; private rawMarkdownIt_: any;
public constructor(options: Options = null) { public constructor(options: Options = null) {
this.options_ = { this.options_ = {
ResourceModel: { ResourceModel: defaultResourceModel,
isResourceUrl: () => false,
},
isSafeMode: false, isSafeMode: false,
...options, ...options,
}; };

View File

@ -1,11 +1,10 @@
import InMemoryCache from './InMemoryCache'; import InMemoryCache from './InMemoryCache';
import noteStyle from './noteStyle'; import noteStyle from './noteStyle';
import { fileExtension } from './pathUtils'; import { fileExtension } from '@joplin/utils/path';
import setupLinkify from './MdToHtml/setupLinkify'; import setupLinkify from './MdToHtml/setupLinkify';
import validateLinks from './MdToHtml/validateLinks'; import validateLinks from './MdToHtml/validateLinks';
import { ItemIdToUrlHandler } from './utils';
import { RenderResult, RenderResultPluginAsset } from './MarkupToHtml';
import { Options as NoteStyleOptions } from './noteStyle'; import { Options as NoteStyleOptions } from './noteStyle';
import { FsDriver, ItemIdToUrlHandler, MarkupRenderer, OptionsResourceModel, RenderOptions, RenderResult, RenderResultPluginAsset } from './types';
import hljs from './highlight'; import hljs from './highlight';
import * as MarkdownIt from 'markdown-it'; import * as MarkdownIt from 'markdown-it';
@ -13,28 +12,6 @@ const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = new Entities().encode; const htmlentities = new Entities().encode;
const md5 = require('md5'); 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 { interface RendererRule {
install(context: any, ruleOptions: any): any; install(context: any, ruleOptions: any): any;
assets?(theme: any): any; assets?(theme: any): any;
@ -109,10 +86,10 @@ export interface ExtraRendererRule {
export interface Options { export interface Options {
resourceBaseUrl?: string; resourceBaseUrl?: string;
ResourceModel?: any; ResourceModel?: OptionsResourceModel;
pluginOptions?: any; pluginOptions?: any;
tempDir?: string; tempDir?: string;
fsDriver?: any; fsDriver?: FsDriver;
extraRendererRules?: ExtraRendererRule[]; extraRendererRules?: ExtraRendererRule[];
customCss?: string; customCss?: string;
} }
@ -150,7 +127,7 @@ export interface RuleOptions {
context: PluginContext; context: PluginContext;
theme: any; theme: any;
postMessageSyntax: string; postMessageSyntax: string;
ResourceModel: any; ResourceModel: OptionsResourceModel;
resourceBaseUrl: string; resourceBaseUrl: string;
resources: any; // resourceId: Resource resources: any; // resourceId: Resource
@ -199,12 +176,12 @@ export interface RuleOptions {
platformName?: string; platformName?: string;
} }
export default class MdToHtml { export default class MdToHtml implements MarkupRenderer {
private resourceBaseUrl_: string; private resourceBaseUrl_: string;
private ResourceModel_: any; private ResourceModel_: OptionsResourceModel;
private contextCache_: any; private contextCache_: any;
private fsDriver_: any; private fsDriver_: FsDriver;
private cachedOutputs_: any = {}; private cachedOutputs_: any = {};
private lastCodeHighlightCacheKey_: string = null; private lastCodeHighlightCacheKey_: string = null;

View File

@ -1,6 +1,6 @@
import utils from '../utils'; import * as utils from '../utils';
export interface Options { export interface Options {

View File

@ -1,3 +1,4 @@
import defaultResourceModel from '../defaultResourceModel';
import linkReplacement from './linkReplacement'; import linkReplacement from './linkReplacement';
import { describe, test, expect } from '@jest/globals'; import { describe, test, expect } from '@jest/globals';
@ -24,7 +25,7 @@ describe('linkReplacement', () => {
const resourceId = 'f6afba55bdf74568ac94f8d1e3578d2c'; const resourceId = 'f6afba55bdf74568ac94f8d1e3578d2c';
const r = linkReplacement(`:/${resourceId}`, { const r = linkReplacement(`:/${resourceId}`, {
ResourceModel: {}, ResourceModel: defaultResourceModel,
resources: { resources: {
[resourceId]: { [resourceId]: {
item: {}, item: {},
@ -42,7 +43,7 @@ describe('linkReplacement', () => {
const resourceId = 'f6afba55bdf74568ac94f8d1e3578d2c'; const resourceId = 'f6afba55bdf74568ac94f8d1e3578d2c';
const r = linkReplacement(`:/${resourceId}`, { const r = linkReplacement(`:/${resourceId}`, {
ResourceModel: {}, ResourceModel: defaultResourceModel,
resources: { resources: {
[resourceId]: { [resourceId]: {
item: {}, item: {},
@ -62,7 +63,7 @@ describe('linkReplacement', () => {
const resourceId = 'e6afba55bdf74568ac94f8d1e3578d2c'; const resourceId = 'e6afba55bdf74568ac94f8d1e3578d2c';
const linkHtml = linkReplacement(`:/${resourceId}`, { const linkHtml = linkReplacement(`:/${resourceId}`, {
ResourceModel: {}, ResourceModel: defaultResourceModel,
resources: { resources: {
[resourceId]: { [resourceId]: {
item: {}, item: {},

View File

@ -1,4 +1,5 @@
import utils, { ItemIdToUrlHandler } from '../utils'; import { ItemIdToUrlHandler, OptionsResourceModel } from '../types';
import * as utils from '../utils';
import createEventHandlingAttrs from './createEventHandlingAttrs'; import createEventHandlingAttrs from './createEventHandlingAttrs';
const Entities = require('html-entities').AllHtmlEntities; const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = new Entities().encode; const htmlentities = new Entities().encode;
@ -8,7 +9,7 @@ const { getClassNameForMimeType } = require('font-awesome-filetypes');
export interface Options { export interface Options {
title?: string; title?: string;
resources?: any; resources?: any;
ResourceModel?: any; ResourceModel?: OptionsResourceModel;
linkRenderingType?: number; linkRenderingType?: number;
plainResourceRendering?: boolean; plainResourceRendering?: boolean;
postMessageSyntax?: string; postMessageSyntax?: string;

View File

@ -1,5 +1,5 @@
import { Link } from '../MdToHtml'; import { Link } from '../MdToHtml';
import { toForwardSlashes } from '../pathUtils'; import { toForwardSlashes } from '@joplin/utils/path';
import { LinkIndexes } from './rules/link_close'; import { LinkIndexes } from './rules/link_close';
const Entities = require('html-entities').AllHtmlEntities; const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = new Entities().encode; const htmlentities = new Entities().encode;

View File

@ -1,6 +1,6 @@
import { RuleOptions } from '../../MdToHtml'; import { RuleOptions } from '../../MdToHtml';
import { attributesHtml } from '../../htmlUtils'; import { attributesHtml } from '../../htmlUtils';
import utils from '../../utils'; import * as utils from '../../utils';
function renderImageHtml(before: string, src: string, after: string, ruleOptions: RuleOptions) { function renderImageHtml(before: string, src: string, after: string, ruleOptions: RuleOptions) {
const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl, ruleOptions.itemIdToUrl); const r = utils.imageReplacement(ruleOptions.ResourceModel, src, ruleOptions.resources, ruleOptions.resourceBaseUrl, ruleOptions.itemIdToUrl);

View File

@ -1,6 +1,6 @@
import { RuleOptions } from '../../MdToHtml'; import { RuleOptions } from '../../MdToHtml';
import { attributesHtml } from '../../htmlUtils'; import { attributesHtml } from '../../htmlUtils';
import utils from '../../utils'; import * as utils from '../../utils';
import createEventHandlingAttrs from '../createEventHandlingAttrs'; import createEventHandlingAttrs from '../createEventHandlingAttrs';
function plugin(markdownIt: any, ruleOptions: RuleOptions) { function plugin(markdownIt: any, ruleOptions: RuleOptions) {

View File

@ -1,6 +1,6 @@
import { RuleOptions } from '../../MdToHtml'; import { RuleOptions } from '../../MdToHtml';
import linkReplacement from '../linkReplacement'; import linkReplacement from '../linkReplacement';
import utils from '../../utils'; import * as utils from '../../utils';
const urlUtils = require('../../urlUtils.js'); const urlUtils = require('../../urlUtils.js');

View File

@ -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 // Utility function to convert the plugin assets to a list of LINK or SCRIPT tags
// that can be included in the HEAD tag. // 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 }; options = { asHtml: false, ...options };
const headers = {}; const headers: Record<string, string> = {};
for (let i = 0; i < pluginAssets.length; i++) { for (let i = 0; i < pluginAssets.length; i++) {
const asset = pluginAssets[i]; const asset = pluginAssets[i];
if (asset.mime === 'text/css') { if (asset.mime === 'text/css') {
@ -23,6 +29,6 @@ function assetsToHeaders(pluginAssets, options = null) {
} }
return headers; return headers;
} };
module.exports = assetsToHeaders; export default assetsToHeaders;

View 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;

View File

@ -1,11 +1,11 @@
import MarkupToHtml, { MarkupLanguage } from './MarkupToHtml'; import MarkupToHtml, { MarkupLanguage } from './MarkupToHtml';
import MdToHtml from './MdToHtml'; import MdToHtml from './MdToHtml';
import HtmlToHtml from './HtmlToHtml'; import HtmlToHtml from './HtmlToHtml';
import utils from './utils'; import * as utils from './utils';
import setupLinkify from './MdToHtml/setupLinkify'; import setupLinkify from './MdToHtml/setupLinkify';
import validateLinks from './MdToHtml/validateLinks'; import validateLinks from './MdToHtml/validateLinks';
import headerAnchor from './headerAnchor'; import headerAnchor from './headerAnchor';
const assetsToHeaders = require('./assetsToHeaders'); import assetsToHeaders from './assetsToHeaders';
export { export {
MarkupToHtml, MarkupToHtml,

View File

@ -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, '/');
}

View 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;
}

View File

@ -1,3 +1,5 @@
import { ItemIdToUrlHandler, OptionsResourceModel } from './types';
const Entities = require('html-entities').AllHtmlEntities; const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = new Entities().encode; const htmlentities = new Entities().encode;
@ -9,16 +11,14 @@ const FetchStatuses = {
FETCH_STATUS_ERROR: 3, FETCH_STATUS_ERROR: 3,
}; };
const utils: any = {}; export const getAttr = function(attrs: string[], name: string, defaultValue: string = null) {
utils.getAttr = function(attrs: string[], name: string, defaultValue: string = null) {
for (let i = 0; i < attrs.length; i++) { for (let i = 0; i < attrs.length; i++) {
if (attrs[i][0] === name) return attrs[i].length > 1 ? attrs[i][1] : null; if (attrs[i][0] === name) return attrs[i].length > 1 ? attrs[i][1] : null;
} }
return defaultValue; return defaultValue;
}; };
utils.notDownloadedResource = function() { export const notDownloadedResource = function() {
return ` return `
<svg width="1700" height="1536" xmlns="http://www.w3.org/2000/svg"> <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"/> <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 // https://github.com/ForkAwesome/Fork-Awesome/blob/master/src/icons/svg/file-image-o.svg
// Height changed to 1795 // Height changed to 1795
return ` 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 // https://github.com/ForkAwesome/Fork-Awesome/blob/master/src/icons/svg/file-o.svg
return ` return `
<svg width="1925" height="1792" xmlns="http://www.w3.org/2000/svg"> <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 // https://github.com/ForkAwesome/Fork-Awesome/blob/master/src/icons/svg/times-circle.svg
return ` return `
<svg width="1795" height="1795" xmlns="http://www.w3.org/2000/svg"> <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 // https://github.com/ForkAwesome/Fork-Awesome/blob/master/src/icons/svg/hourglass-half.svg
return ` return `
<svg width="1536" height="1790" xmlns="http://www.w3.org/2000/svg"> <svg width="1536" height="1790" xmlns="http://www.w3.org/2000/svg">
@ -63,21 +63,21 @@ utils.loaderImage = function() {
`; `;
}; };
utils.resourceStatusImage = function(status: string) { export const resourceStatusImage = function(status: string) {
if (status === 'notDownloaded') return utils.notDownloadedResource(); if (status === 'notDownloaded') return notDownloadedResource();
return utils.resourceStatusFile(status); return resourceStatusFile(status);
}; };
utils.resourceStatusFile = function(status: string) { export const resourceStatusFile = function(status: string) {
if (status === 'notDownloaded') return utils.notDownloadedResource(); if (status === 'notDownloaded') return notDownloadedResource();
if (status === 'downloading') return utils.loaderImage(); if (status === 'downloading') return loaderImage();
if (status === 'encrypted') return utils.loaderImage(); if (status === 'encrypted') return loaderImage();
if (status === 'error') return utils.errorImage(); if (status === 'error') return errorImage();
throw new Error(`Unknown status: ${status}`); throw new Error(`Unknown status: ${status}`);
}; };
utils.resourceStatusIndex = function(status: string) { export const resourceStatusIndex = function(status: string) {
if (status === 'error') return -1; if (status === 'error') return -1;
if (status === 'notDownloaded') return 0; if (status === 'notDownloaded') return 0;
if (status === 'downloading') return 1; if (status === 'downloading') return 1;
@ -87,7 +87,7 @@ utils.resourceStatusIndex = function(status: string) {
throw new Error(`Unknown status: ${status}`); throw new Error(`Unknown status: ${status}`);
}; };
utils.resourceStatusName = function(index: number) { export const resourceStatusName = function(index: number) {
if (index === -1) return 'error'; if (index === -1) return 'error';
if (index === 0) return 'notDownloaded'; if (index === 0) return 'notDownloaded';
if (index === 1) return 'downloading'; if (index === 1) return 'downloading';
@ -97,34 +97,32 @@ utils.resourceStatusName = function(index: number) {
throw new Error(`Unknown index: ${index}`); 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'; if (!ResourceModel) return 'ready';
let resourceStatus = 'ready'; let status = 'ready';
if (resourceInfo) { if (resourceInfo) {
const resource = resourceInfo.item; const resource = resourceInfo.item;
const localState = resourceInfo.localState; const localState = resourceInfo.localState;
if (localState.fetch_status === FetchStatuses.FETCH_STATUS_IDLE) { if (localState.fetch_status === FetchStatuses.FETCH_STATUS_IDLE) {
resourceStatus = 'notDownloaded'; status = 'notDownloaded';
} else if (localState.fetch_status === FetchStatuses.FETCH_STATUS_STARTED) { } else if (localState.fetch_status === FetchStatuses.FETCH_STATUS_STARTED) {
resourceStatus = 'downloading'; status = 'downloading';
} else if (localState.fetch_status === FetchStatuses.FETCH_STATUS_DONE) { } else if (localState.fetch_status === FetchStatuses.FETCH_STATUS_DONE) {
if (resource.encryption_blob_encrypted || resource.encryption_applied) { if (resource.encryption_blob_encrypted || resource.encryption_applied) {
resourceStatus = 'encrypted'; status = 'encrypted';
} }
} }
} else { } else {
resourceStatus = 'notDownloaded'; status = 'notDownloaded';
} }
return resourceStatus; return status;
}; };
export type ItemIdToUrlHandler = (resource: any)=> string; export const imageReplacement = function(ResourceModel: OptionsResourceModel, src: string, resources: any, resourceBaseUrl: string, itemIdToUrl: ItemIdToUrlHandler = null) {
utils.imageReplacement = function(ResourceModel: any, src: string, resources: any, resourceBaseUrl: string, itemIdToUrl: ItemIdToUrlHandler = null) {
if (!ResourceModel || !resources) return null; if (!ResourceModel || !resources) return null;
if (!ResourceModel.isResourceUrl(src)) 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 resourceId = ResourceModel.urlToId(src);
const result = resources[resourceId]; const result = resources[resourceId];
const resource = result ? result.item : null; const resource = result ? result.item : null;
const resourceStatus = utils.resourceStatus(ResourceModel, result); const status = resourceStatus(ResourceModel, result);
if (resourceStatus !== 'ready') { if (status !== 'ready') {
const icon = utils.resourceStatusImage(resourceStatus); const icon = resourceStatusImage(status);
return `<div class="not-loaded-resource resource-status-${resourceStatus}" data-resource-id="${resourceId}">` + `<img src="data:image/svg+xml;utf8,${htmlentities(icon)}"/>` + '</div>'; 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() : ''; const mime = resource.mime ? resource.mime.toLowerCase() : '';
if (ResourceModel.isSupportedImageMimeType(mime)) { 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 // Used in mobile app when enableLongPress = true. Tells for how long
// the resource should be pressed before the menu is shown. // the resource should be pressed before the menu is shown.
utils.longPressDelay = 500; export const longPressDelay = 500;
export default utils;

View File

@ -16,7 +16,7 @@ import { NoteEntity } from '@joplin/lib/services/database/types';
import { formatDateTime } from './time'; import { formatDateTime } from './time';
import { ErrorBadRequest, ErrorForbidden, ErrorNotFound } from './errors'; import { ErrorBadRequest, ErrorForbidden, ErrorNotFound } from './errors';
import { MarkupToHtml } from '@joplin/renderer'; 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'; import { isValidHeaderIdentifier } from '@joplin/lib/services/e2ee/EncryptionService';
const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js'); const { DatabaseDriverNode } = require('@joplin/lib/database-driver-node.js');
import { themeStyle } from '@joplin/lib/theme'; import { themeStyle } from '@joplin/lib/theme';

View File

@ -13,7 +13,8 @@
"./net": "./dist/net.js", "./net": "./dist/net.js",
"./time": "./dist/time.js", "./time": "./dist/time.js",
"./types": "./dist/types.js", "./types": "./dist/types.js",
"./url": "./dist/url.js" "./url": "./dist/url.js",
"./path": "./dist/path.js"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"

View 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
View 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;
}