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:
parent
352ee6496e
commit
f5e1e45f6f
@ -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
5
.gitignore
vendored
@ -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
|
||||||
|
@ -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', () => {
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
@ -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';
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
@ -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> {
|
||||||
|
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*/
|
/* 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;
|
|
||||||
}
|
|
||||||
|
@ -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]);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -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 {
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
import utils from '../utils';
|
import * as utils from '../utils';
|
||||||
|
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
|
@ -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: {},
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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');
|
||||||
|
|
||||||
|
@ -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;
|
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 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,
|
||||||
|
@ -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 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;
|
|
||||||
|
@ -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';
|
||||||
|
@ -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"
|
||||||
|
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