You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-24 20:19:10 +02:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
e2bf878953 | ||
|
0b50e77ea3 | ||
|
a5a7bae6c4 | ||
|
36d120ac00 | ||
|
d9bd929e03 | ||
|
e8d1ac4111 |
@@ -1314,6 +1314,9 @@ packages/lib/services/interop/InteropService_Exporter_Jex.js.map
|
||||
packages/lib/services/interop/InteropService_Exporter_Md.d.ts
|
||||
packages/lib/services/interop/InteropService_Exporter_Md.js
|
||||
packages/lib/services/interop/InteropService_Exporter_Md.js.map
|
||||
packages/lib/services/interop/InteropService_Exporter_Md.test.d.ts
|
||||
packages/lib/services/interop/InteropService_Exporter_Md.test.js
|
||||
packages/lib/services/interop/InteropService_Exporter_Md.test.js.map
|
||||
packages/lib/services/interop/InteropService_Exporter_Md_frontmatter.d.ts
|
||||
packages/lib/services/interop/InteropService_Exporter_Md_frontmatter.js
|
||||
packages/lib/services/interop/InteropService_Exporter_Md_frontmatter.js.map
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1297,6 +1297,9 @@ packages/lib/services/interop/InteropService_Exporter_Jex.js.map
|
||||
packages/lib/services/interop/InteropService_Exporter_Md.d.ts
|
||||
packages/lib/services/interop/InteropService_Exporter_Md.js
|
||||
packages/lib/services/interop/InteropService_Exporter_Md.js.map
|
||||
packages/lib/services/interop/InteropService_Exporter_Md.test.d.ts
|
||||
packages/lib/services/interop/InteropService_Exporter_Md.test.js
|
||||
packages/lib/services/interop/InteropService_Exporter_Md.test.js.map
|
||||
packages/lib/services/interop/InteropService_Exporter_Md_frontmatter.d.ts
|
||||
packages/lib/services/interop/InteropService_Exporter_Md_frontmatter.js
|
||||
packages/lib/services/interop/InteropService_Exporter_Md_frontmatter.js.map
|
||||
|
@@ -0,0 +1,115 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.translateScrollPercentToViewer = exports.translateScrollPercentToEditor = void 0;
|
||||
const react_1 = require('react');
|
||||
const shim_1 = require('@joplin/lib/shim');
|
||||
function useScrollHandler(editorRef, webviewRef, onScroll) {
|
||||
const ignoreNextEditorScrollEvent_ = react_1.useRef(false);
|
||||
const scrollTimeoutId_ = react_1.useRef(null);
|
||||
const scheduleOnScroll = react_1.useCallback((event) => {
|
||||
if (scrollTimeoutId_.current) {
|
||||
shim_1.default.clearTimeout(scrollTimeoutId_.current);
|
||||
scrollTimeoutId_.current = null;
|
||||
}
|
||||
scrollTimeoutId_.current = shim_1.default.setTimeout(() => {
|
||||
scrollTimeoutId_.current = null;
|
||||
onScroll(event);
|
||||
}, 10);
|
||||
}, [onScroll]);
|
||||
const setEditorPercentScroll = react_1.useCallback((p) => {
|
||||
ignoreNextEditorScrollEvent_.current = true;
|
||||
if (editorRef.current) {
|
||||
editorRef.current.setScrollPercent(p);
|
||||
scheduleOnScroll({ percent: p });
|
||||
}
|
||||
}, [scheduleOnScroll]);
|
||||
const setViewerPercentScroll = react_1.useCallback((p) => {
|
||||
if (webviewRef.current) {
|
||||
webviewRef.current.wrappedInstance.send('setPercentScroll', p);
|
||||
scheduleOnScroll({ percent: p });
|
||||
}
|
||||
}, [scheduleOnScroll]);
|
||||
const editor_scroll = react_1.useCallback(() => {
|
||||
if (ignoreNextEditorScrollEvent_.current) {
|
||||
ignoreNextEditorScrollEvent_.current = false;
|
||||
return;
|
||||
}
|
||||
if (editorRef.current) {
|
||||
const editorPercent = Math.max(0, Math.min(1, editorRef.current.getScrollPercent()));
|
||||
if (!isNaN(editorPercent)) {
|
||||
// when switching to another note, the percent can sometimes be NaN
|
||||
// this is coming from `gui/NoteEditor/NoteBody/CodeMirror/utils/useScrollUtils.ts`
|
||||
// when CodeMirror returns scroll info with heigth == clientHeigth
|
||||
// https://github.com/laurent22/joplin/issues/4797
|
||||
const viewerPercent = exports.translateScrollPercentToViewer(editorRef, webviewRef, editorPercent);
|
||||
setViewerPercentScroll(viewerPercent);
|
||||
}
|
||||
}
|
||||
}, [setViewerPercentScroll]);
|
||||
const resetScroll = react_1.useCallback(() => {
|
||||
if (editorRef.current) {
|
||||
editorRef.current.setScrollPercent(0);
|
||||
}
|
||||
}, []);
|
||||
return { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll };
|
||||
}
|
||||
exports.default = useScrollHandler;
|
||||
const translateScrollPercent_ = (editorRef, webviewRef, percent, editorToViewer) => {
|
||||
let _a;
|
||||
// If the input is out of (0,1) or not number, it is not translated.
|
||||
if (!(0 < percent && percent < 1)) { return percent; }
|
||||
const map = (_a = webviewRef.current) === null || _a === void 0 ? void 0 : _a.wrappedInstance.getSyncScrollMap();
|
||||
const cm = editorRef.current;
|
||||
if (!map || map.line.length <= 2 || !cm) { return percent; } // No translation
|
||||
const lineCount = cm.lineCount();
|
||||
if (map.line[map.line.length - 2] >= lineCount) {
|
||||
// Discarded a obsolete map and use no translation.
|
||||
webviewRef.current.wrappedInstance.refreshSyncScrollMap(false);
|
||||
return percent;
|
||||
}
|
||||
const info = cm.getScrollInfo();
|
||||
const height = Math.max(1, info.height - info.clientHeight);
|
||||
let values = map.percent, target = percent;
|
||||
if (editorToViewer) {
|
||||
const top = percent * height;
|
||||
const line = cm.lineAtHeight(top, 'local');
|
||||
values = map.line;
|
||||
target = line;
|
||||
}
|
||||
// Binary search (rightmost): finds where map[r-1][field] <= target < map[r][field]
|
||||
let l = 1, r = values.length - 1;
|
||||
while (l < r) {
|
||||
const m = Math.floor(l + (r - l) / 2);
|
||||
if (target < values[m]) { r = m; } else { l = m + 1; }
|
||||
}
|
||||
const lineU = map.line[r - 1];
|
||||
const lineL = Math.min(lineCount, map.line[r]);
|
||||
const ePercentU = r == 1 ? 0 : Math.min(1, cm.heightAtLine(lineU, 'local') / height);
|
||||
const ePercentL = Math.min(1, cm.heightAtLine(lineL, 'local') / height);
|
||||
const vPercentU = map.percent[r - 1];
|
||||
const vPercentL = ePercentL == 1 ? 1 : map.percent[r];
|
||||
let result;
|
||||
if (editorToViewer) {
|
||||
const linInterp = (percent - ePercentU) / (ePercentL - ePercentU);
|
||||
result = vPercentU + (vPercentL - vPercentU) * linInterp;
|
||||
} else {
|
||||
const linInterp = (percent - vPercentU) / (vPercentL - vPercentU);
|
||||
result = ePercentU + (ePercentL - ePercentU) * linInterp;
|
||||
}
|
||||
return Math.max(0, Math.min(1, result));
|
||||
};
|
||||
// translateScrollPercentToEditor() and translateScrollPercentToViewer() are
|
||||
// the translation functions between Editor's scroll percent and Viewer's scroll
|
||||
// percent. They are used for synchronous scrolling between Editor and Viewer.
|
||||
// They use a SyncScrollMap provided by Viewer for its translation.
|
||||
// To see the detail of synchronous scrolling, refer the following design document.
|
||||
// https://github.com/laurent22/joplin/pull/5512#issuecomment-931277022
|
||||
exports.translateScrollPercentToEditor = (editorRef, webviewRef, viewerPercent) => {
|
||||
const editorPercent = translateScrollPercent_(editorRef, webviewRef, viewerPercent, false);
|
||||
return editorPercent;
|
||||
};
|
||||
exports.translateScrollPercentToViewer = (editorRef, webviewRef, editorPercent) => {
|
||||
const viewerPercent = translateScrollPercent_(editorRef, webviewRef, editorPercent, true);
|
||||
return viewerPercent;
|
||||
};
|
||||
// # sourceMappingURL=useScrollHandler.js.map
|
83
packages/app-desktop/gui/utils/SyncScrollMap.js
Normal file
83
packages/app-desktop/gui/utils/SyncScrollMap.js
Normal file
@@ -0,0 +1,83 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.SyncScrollMapper = void 0;
|
||||
const shim_1 = require('@joplin/lib/shim');
|
||||
// Map creation utility class
|
||||
class SyncScrollMapper {
|
||||
constructor() {
|
||||
this.map_ = null;
|
||||
this.refreshTimeoutId_ = null;
|
||||
this.refreshTime_ = 0;
|
||||
}
|
||||
// Invalidates an outdated SyncScrollMap.
|
||||
// For a performance reason, too frequent refresh requests are
|
||||
// skippend and delayed. If forced is true, refreshing is immediately performed.
|
||||
refresh(forced) {
|
||||
const elapsed = this.refreshTime_ ? Date.now() - this.refreshTime_ : 10 * 1000;
|
||||
if (!forced && (elapsed < 200 || this.refreshTimeoutId_)) {
|
||||
// to avoid too frequent recreations of a sync-scroll map.
|
||||
if (this.refreshTimeoutId_) {
|
||||
shim_1.default.clearTimeout(this.refreshTimeoutId_);
|
||||
this.refreshTimeoutId_ = null;
|
||||
}
|
||||
this.refreshTimeoutId_ = shim_1.default.setTimeout(() => {
|
||||
this.refreshTimeoutId_ = null;
|
||||
this.map_ = null;
|
||||
this.refreshTime_ = Date.now();
|
||||
}, 200);
|
||||
} else {
|
||||
this.map_ = null;
|
||||
this.refreshTime_ = Date.now();
|
||||
}
|
||||
}
|
||||
// Creates a new SyncScrollMap or reuses an existing one.
|
||||
get(doc) {
|
||||
// Returns a cached translation map between editor's scroll percenet
|
||||
// and viewer's scroll percent. Both attributes (line and percent) of
|
||||
// the returned map are sorted respectively.
|
||||
// Since creating this map is costly for each scroll event, it is cached.
|
||||
// When some update events which outdate it such as switching a note or
|
||||
// editing a note, it has to be invalidated (using refresh()),
|
||||
// and a new map will be created at a next scroll event.
|
||||
if (!doc) { return null; }
|
||||
const contentElement = doc.getElementById('joplin-container-content');
|
||||
if (!contentElement) { return null; }
|
||||
const height = Math.max(1, contentElement.scrollHeight - contentElement.clientHeight);
|
||||
if (this.map_) {
|
||||
// check whether map_ is obsolete
|
||||
if (this.map_.viewHeight === height) { return this.map_; }
|
||||
this.map_ = null;
|
||||
}
|
||||
// Since getBoundingClientRect() returns a relative position,
|
||||
// the offset of the origin is needed to get its aboslute position.
|
||||
const offset = doc.getElementById('rendered-md').getBoundingClientRect().top;
|
||||
if (!offset) { return null; }
|
||||
// Mapping information between editor's lines and viewer's elements is
|
||||
// embedded into elements by the renderer.
|
||||
// See also renderer/MdToHtml/rules/source_map.ts.
|
||||
const elems = doc.getElementsByClassName('maps-to-line');
|
||||
const map = { line: [0], percent: [0], viewHeight: height };
|
||||
// Each map entry is total-ordered.
|
||||
let last = 0;
|
||||
for (let i = 0; i < elems.length; i++) {
|
||||
const top = elems[i].getBoundingClientRect().top - offset;
|
||||
const line = Number(elems[i].getAttribute('source-line'));
|
||||
const percent = Math.max(0, Math.min(1, top / height));
|
||||
if (map.line[last] < line && map.percent[last] < percent) {
|
||||
map.line.push(line);
|
||||
map.percent.push(percent);
|
||||
last += 1;
|
||||
}
|
||||
}
|
||||
if (map.percent[last] < 1) {
|
||||
map.line.push(1e10);
|
||||
map.percent.push(1);
|
||||
} else {
|
||||
map.line[last] = 1e10;
|
||||
}
|
||||
this.map_ = map;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
exports.SyncScrollMapper = SyncScrollMapper;
|
||||
// # sourceMappingURL=SyncScrollMap.js.map
|
4
packages/app-desktop/package-lock.json
generated
4
packages/app-desktop/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.5.10",
|
||||
"version": "2.5.11",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.5.10",
|
||||
"version": "2.5.11",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@electron/remote": "^2.0.1",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.5.10",
|
||||
"version": "2.5.11",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
@@ -492,13 +492,13 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 74;
|
||||
CURRENT_PROJECT_VERSION = 77;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 12.5.0;
|
||||
MARKETING_VERSION = 12.5.3;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -521,12 +521,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Joplin/Joplin.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 74;
|
||||
CURRENT_PROJECT_VERSION = 77;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
INFOPLIST_FILE = Joplin/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 12.5.0;
|
||||
MARKETING_VERSION = 12.5.3;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
@@ -667,14 +667,14 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 74;
|
||||
CURRENT_PROJECT_VERSION = 77;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 12.5.0;
|
||||
MARKETING_VERSION = 12.5.3;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
@@ -698,14 +698,14 @@
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 74;
|
||||
CURRENT_PROJECT_VERSION = 77;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = A9BXAFS6CT;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
MARKETING_VERSION = 12.5.0;
|
||||
MARKETING_VERSION = 12.5.3;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = net.cozic.joplin.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
@@ -317,7 +317,7 @@ PODS:
|
||||
- React
|
||||
- RNSecureRandom (1.0.0-rc.0):
|
||||
- React
|
||||
- RNShare (5.1.5):
|
||||
- RNShare (7.2.1):
|
||||
- React-Core
|
||||
- RNVectorIcons (7.1.0):
|
||||
- React
|
||||
@@ -540,7 +540,7 @@ SPEC CHECKSUMS:
|
||||
RNFS: 2bd9eb49dc82fa9676382f0585b992c424cd59df
|
||||
RNQuickAction: 6d404a869dc872cde841ad3147416a670d13fa93
|
||||
RNSecureRandom: 1f19ad1492f7ed416b8fc79e92216a1f73f13a4c
|
||||
RNShare: 9cdd23357981cf4dee275eb79239e860dccc0faf
|
||||
RNShare: edd621a71124961e29a7ba43a84bd1c6f9980d88
|
||||
RNVectorIcons: bc69e6a278b14842063605de32bec61f0b251a59
|
||||
Yoga: 575c581c63e0d35c9a83f4b46d01d63abc1100ac
|
||||
|
||||
|
7450
packages/app-mobile/package-lock.json
generated
7450
packages/app-mobile/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -50,7 +50,7 @@
|
||||
"react-native-quick-actions": "^0.3.13",
|
||||
"react-native-rsa-native": "^2.0.4",
|
||||
"react-native-securerandom": "^1.0.0-rc.0",
|
||||
"react-native-share": "^5.1.5",
|
||||
"react-native-share": "^7.2.1",
|
||||
"react-native-side-menu": "^1.1.3",
|
||||
"react-native-sqlite-storage": "^5.0.0",
|
||||
"react-native-vector-icons": "^7.1.0",
|
||||
|
@@ -1,15 +1,15 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const { setupDatabaseAndSynchronizer, switchClient, exportDir, supportDir } = require('../../testing/test-utils.js');
|
||||
const InteropService_Exporter_Md = require('../../services/interop/InteropService_Exporter_Md').default;
|
||||
const BaseModel = require('../../BaseModel').default;
|
||||
const Folder = require('../../models/Folder').default;
|
||||
const Resource = require('../../models/Resource').default;
|
||||
const Note = require('../../models/Note').default;
|
||||
const shim = require('../../shim').default;
|
||||
const { MarkupToHtml } = require('@joplin/renderer');
|
||||
import * as fs from 'fs-extra';
|
||||
import { setupDatabaseAndSynchronizer, switchClient, exportDir, supportDir } from '../../testing/test-utils.js';
|
||||
import InteropService_Exporter_Md from '../../services/interop/InteropService_Exporter_Md';
|
||||
import BaseModel from '../../BaseModel';
|
||||
import Folder from '../../models/Folder';
|
||||
import Resource from '../../models/Resource';
|
||||
import Note from '../../models/Note';
|
||||
import shim from '../../shim';
|
||||
import { MarkupToHtml } from '@joplin/renderer';
|
||||
import { NoteEntity, ResourceEntity } from '../database/types.js';
|
||||
import InteropService from './InteropService.js';
|
||||
import { fileExtension } from '../../path-utils.js';
|
||||
|
||||
describe('interop/InteropService_Exporter_Md', function() {
|
||||
|
||||
@@ -33,8 +33,8 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir());
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
const itemsToExport: any[] = [];
|
||||
const queueExportItem = (itemType: number, itemOrId: any) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
@@ -59,13 +59,13 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
queueExportItem(BaseModel.TYPE_NOTE, note3);
|
||||
queueExportItem(BaseModel.TYPE_RESOURCE, (await Note.linkedResourceIds(note3.body))[0]);
|
||||
|
||||
expect(!exporter.context() && !(exporter.context().notePaths || Object.keys(exporter.context().notePaths).length)).toBe(false, 'Context should be empty before processing.');
|
||||
expect(!exporter.context() && !(exporter.context().notePaths || Object.keys(exporter.context().notePaths).length)).toBe(false);
|
||||
|
||||
await exporter.processItem(Folder.modelType(), folder1);
|
||||
await exporter.processItem(Folder.modelType(), folder2);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(3, 'There should be 3 note paths in the context.');
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(3);
|
||||
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1.md');
|
||||
expect(exporter.context().notePaths[note2.id]).toBe('folder1/note2.md');
|
||||
expect(exporter.context().notePaths[note3.id]).toBe('folder2/note3.html');
|
||||
@@ -75,8 +75,8 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir());
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
const itemsToExport: any[] = [];
|
||||
const queueExportItem = (itemType: number, itemOrId: any) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
@@ -110,9 +110,9 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
await exporter.processResource(resource1, Resource.fullPath(resource1));
|
||||
await exporter.processResource(resource2, Resource.fullPath(resource2));
|
||||
|
||||
expect(!exporter.context() && !(exporter.context().destResourcePaths || Object.keys(exporter.context().destResourcePaths).length)).toBe(false, 'Context should be empty before processing.');
|
||||
expect(!exporter.context() && !(exporter.context().destResourcePaths || Object.keys(exporter.context().destResourcePaths).length)).toBe(false);
|
||||
|
||||
expect(Object.keys(exporter.context().destResourcePaths).length).toBe(2, 'There should be 2 resource paths in the context.');
|
||||
expect(Object.keys(exporter.context().destResourcePaths).length).toBe(2);
|
||||
expect(exporter.context().destResourcePaths[resource1.id]).toBe(`${exportDir()}/_resources/photo.jpg`);
|
||||
expect(exporter.context().destResourcePaths[resource2.id]).toBe(`${exportDir()}/_resources/photo-1.jpg`);
|
||||
}));
|
||||
@@ -121,8 +121,8 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir());
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
const itemsToExport: any[] = [];
|
||||
const queueExportItem = (itemType: number, itemOrId: any) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
@@ -139,7 +139,7 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
await exporter.processItem(Folder.modelType(), folder1);
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(2, 'There should be 2 note paths in the context.');
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(2);
|
||||
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1.md');
|
||||
expect(exporter.context().notePaths[note1_2.id]).toBe('folder1/note1-1.md');
|
||||
}));
|
||||
@@ -148,8 +148,8 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir());
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
const itemsToExport: any[] = [];
|
||||
const queueExportItem = (itemType: number, itemOrId: any) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
@@ -167,7 +167,7 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(1, 'There should be 1 note paths in the context.');
|
||||
expect(Object.keys(exporter.context().notePaths).length).toBe(1);
|
||||
expect(exporter.context().notePaths[note1.id]).toBe('folder1/note1-1.md');
|
||||
}));
|
||||
|
||||
@@ -175,8 +175,8 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir());
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
const itemsToExport: any[] = [];
|
||||
const queueExportItem = (itemType: number, itemOrId: any) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
@@ -204,16 +204,16 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
await exporter.processResource(resource1, Resource.fullPath(resource1));
|
||||
await exporter.processResource(resource2, Resource.fullPath(resource2));
|
||||
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/_resources/photo.jpg`)).toBe(true, 'Resource file should be copied to _resources directory.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/_resources/photo-1.jpg`)).toBe(true, 'Resource file should be copied to _resources directory.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/_resources/photo.jpg`)).toBe(true);
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/_resources/photo-1.jpg`)).toBe(true);
|
||||
}));
|
||||
|
||||
it('should create folders in fs', (async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir());
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
const itemsToExport: any[] = [];
|
||||
const queueExportItem = (itemType: number, itemOrId: any) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
@@ -234,17 +234,17 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
await exporter.prepareForProcessingItemType(BaseModel.TYPE_NOTE, itemsToExport);
|
||||
await exporter.processItem(Note.modelType(), note2);
|
||||
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/folder1`)).toBe(true, 'Folder should be created in filesystem.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/folder1/folder2`)).toBe(true, 'Folder should be created in filesystem.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/folder1/folder3`)).toBe(true, 'Folder should be created in filesystem.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/folder1`)).toBe(true);
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/folder1/folder2`)).toBe(true);
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/folder1/folder3`)).toBe(true);
|
||||
}));
|
||||
|
||||
it('should save notes in fs', (async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir());
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
const itemsToExport: any[] = [];
|
||||
const queueExportItem = (itemType: number, itemOrId: any) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
@@ -271,17 +271,17 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
await exporter.processItem(Note.modelType(), note2);
|
||||
await exporter.processItem(Note.modelType(), note3);
|
||||
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/${exporter.context().notePaths[note1.id]}`)).toBe(true, 'File should be saved in filesystem.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/${exporter.context().notePaths[note2.id]}`)).toBe(true, 'File should be saved in filesystem.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/${exporter.context().notePaths[note3.id]}`)).toBe(true, 'File should be saved in filesystem.');
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/${exporter.context().notePaths[note1.id]}`)).toBe(true);
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/${exporter.context().notePaths[note2.id]}`)).toBe(true);
|
||||
expect(await shim.fsDriver().exists(`${exportDir()}/${exporter.context().notePaths[note3.id]}`)).toBe(true);
|
||||
}));
|
||||
|
||||
it('should replace resource ids with relative paths', (async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir());
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
const itemsToExport: any[] = [];
|
||||
const queueExportItem = (itemType: number, itemOrId: any) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
@@ -325,7 +325,7 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
await exporter.processResource(resource2, Resource.fullPath(resource2));
|
||||
await exporter.processResource(resource3, Resource.fullPath(resource3));
|
||||
await exporter.processResource(resource4, Resource.fullPath(resource3));
|
||||
const context = {
|
||||
const context: any = {
|
||||
resourcePaths: {},
|
||||
};
|
||||
context.resourcePaths[resource1.id] = 'resource1.jpg';
|
||||
@@ -343,25 +343,25 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
const note3_body = await shim.fsDriver().readFile(`${exportDir()}/${exporter.context().notePaths[note3.id]}`);
|
||||
const note4_body = await shim.fsDriver().readFile(`${exportDir()}/${exporter.context().notePaths[note4.id]}`);
|
||||
|
||||
expect(note1_body).toContain('](../_resources/photo.jpg)', 'Resource id should be replaced with a relative path.');
|
||||
expect(note2_body).toContain('](../../_resources/photo-1.jpg)', 'Resource id should be replaced with a relative path.');
|
||||
expect(note3_body).toContain('<img src="../../_resources/photo-2.jpg" alt="alt">', 'Resource id should be replaced with a relative path.');
|
||||
expect(note4_body).toContain('](../../_resources/photo-3.jpg "title")', 'Resource id should be replaced with a relative path.');
|
||||
expect(note1_body).toContain('](../_resources/photo.jpg)');
|
||||
expect(note2_body).toContain('](../../_resources/photo-1.jpg)');
|
||||
expect(note3_body).toContain('<img src="../../_resources/photo-2.jpg" alt="alt">');
|
||||
expect(note4_body).toContain('](../../_resources/photo-3.jpg "title")');
|
||||
}));
|
||||
|
||||
it('should replace note ids with relative paths', (async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir());
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
const itemsToExport: any[] = [];
|
||||
const queueExportItem = (itemType: number, itemOrId: any) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
});
|
||||
};
|
||||
|
||||
const changeNoteBodyAndReload = async (note, newBody) => {
|
||||
const changeNoteBodyAndReload = async (note: NoteEntity, newBody: string) => {
|
||||
note.body = newBody;
|
||||
await Note.save(note);
|
||||
return await Note.load(note.id);
|
||||
@@ -395,18 +395,18 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
const note2_body = await shim.fsDriver().readFile(`${exportDir()}/${exporter.context().notePaths[note2.id]}`);
|
||||
const note3_body = await shim.fsDriver().readFile(`${exportDir()}/${exporter.context().notePaths[note3.id]}`);
|
||||
|
||||
expect(note1_body).toContain('](../folder3/note3.md)', 'Note id should be replaced with a relative path.');
|
||||
expect(note2_body).toContain('](../../folder3/note3.md)', 'Resource id should be replaced with a relative path.');
|
||||
expect(note2_body).toContain('](../../folder1/note1.md)', 'Resource id should be replaced with a relative path.');
|
||||
expect(note3_body).toContain('](../folder1/folder2/note2.md)', 'Resource id should be replaced with a relative path.');
|
||||
expect(note1_body).toContain('](../folder3/note3.md)');
|
||||
expect(note2_body).toContain('](../../folder3/note3.md)');
|
||||
expect(note2_body).toContain('](../../folder1/note1.md)');
|
||||
expect(note3_body).toContain('](../folder1/folder2/note2.md)');
|
||||
}));
|
||||
|
||||
it('should url encode relative note links', (async () => {
|
||||
const exporter = new InteropService_Exporter_Md();
|
||||
await exporter.init(exportDir());
|
||||
|
||||
const itemsToExport = [];
|
||||
const queueExportItem = (itemType, itemOrId) => {
|
||||
const itemsToExport: any[] = [];
|
||||
const queueExportItem = (itemType: number, itemOrId: any) => {
|
||||
itemsToExport.push({
|
||||
type: itemType,
|
||||
itemOrId: itemOrId,
|
||||
@@ -425,6 +425,26 @@ describe('interop/InteropService_Exporter_Md', function() {
|
||||
await exporter.processItem(Note.modelType(), note2);
|
||||
|
||||
const note2_body = await shim.fsDriver().readFile(`${exportDir()}/${exporter.context().notePaths[note2.id]}`);
|
||||
expect(note2_body).toContain('[link](../folder%20with%20space1/note1%20name%20with%20space.md)', 'Whitespace in URL should be encoded');
|
||||
expect(note2_body).toContain('[link](../folder%20with%20space1/note1%20name%20with%20space.md)');
|
||||
}));
|
||||
|
||||
it('should preserve resource file extension', (async () => {
|
||||
const folder = await Folder.save({ title: 'testing' });
|
||||
const note = await Note.save({ title: 'mynote', parent_id: folder.id });
|
||||
await shim.attachFileToNote(note, `${supportDir}/photo.jpg`);
|
||||
|
||||
const resource: ResourceEntity = (await Resource.all())[0];
|
||||
await Resource.save({ id: resource.id, title: 'veryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitleveryverylongtitle.jpg' });
|
||||
|
||||
const service = InteropService.instance();
|
||||
|
||||
await service.export({
|
||||
path: exportDir(),
|
||||
format: 'md',
|
||||
});
|
||||
|
||||
const resourceFilename = (await fs.readdir(`${exportDir()}/_resources`))[0];
|
||||
expect(fileExtension(resourceFilename)).toBe('jpg');
|
||||
}));
|
||||
|
||||
});
|
@@ -143,7 +143,7 @@ export default class InteropService_Exporter_Md extends InteropService_Exporter_
|
||||
if (resource.filename) {
|
||||
fileName = resource.filename;
|
||||
} else if (resource.title) {
|
||||
fileName = friendlySafeFilename(resource.title);
|
||||
fileName = friendlySafeFilename(resource.title, null, true);
|
||||
}
|
||||
|
||||
// Fall back on the resource filename saved in the users resource folder
|
||||
|
@@ -122,6 +122,7 @@ describe('interop/InteropService_Exporter_Md_frontmatter', function() {
|
||||
const content = await exportAndLoad(`${exportDir()}/folder1/Source_title.md`);
|
||||
expect(content).toContain('title: |-\n Source\n title');
|
||||
}));
|
||||
|
||||
test('should not export coordinates if they\'re not available', (async () => {
|
||||
const folder1 = await Folder.save({ title: 'folder1' });
|
||||
await Note.save({ title: 'Coordinates', body: '**ma note**', parent_id: folder1.id });
|
||||
|
35
packages/renderer/MdToHtml/rules/source_map.js
Normal file
35
packages/renderer/MdToHtml/rules/source_map.js
Normal file
@@ -0,0 +1,35 @@
|
||||
'use strict';
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
exports.default = {
|
||||
plugin: (markdownIt, params) => {
|
||||
if (!params.mapsToLine) { return; }
|
||||
const allowedLevels = {
|
||||
paragraph_open: 0,
|
||||
heading_open: 0,
|
||||
// fence: 0, // fence uses custom rendering that doesn't propogate attr so it can't be used for now
|
||||
blockquote_open: 0,
|
||||
table_open: 0,
|
||||
code_block: 0,
|
||||
hr: 0,
|
||||
html_block: 0,
|
||||
list_item_open: 99,
|
||||
math_block: 0,
|
||||
};
|
||||
for (const [key, allowedLevel] of Object.entries(allowedLevels)) {
|
||||
const precedentRule = markdownIt.renderer.rules[key];
|
||||
markdownIt.renderer.rules[key] = (tokens, idx, options, env, self) => {
|
||||
if (!!tokens[idx].map && tokens[idx].level <= allowedLevel) {
|
||||
const line = tokens[idx].map[0];
|
||||
tokens[idx].attrJoin('class', 'maps-to-line');
|
||||
tokens[idx].attrSet('source-line', `${line}`);
|
||||
}
|
||||
if (precedentRule) {
|
||||
return precedentRule(tokens, idx, options, env, self);
|
||||
} else {
|
||||
return self.renderToken(tokens, idx, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
// # sourceMappingURL=source_map.js.map
|
@@ -22,7 +22,7 @@ async function renderItem(context: AppContext, item: Item, share: Share): Promis
|
||||
}
|
||||
|
||||
function createContentDispositionHeader(filename: string) {
|
||||
const encoded = encodeURIComponent(friendlySafeFilename(filename));
|
||||
const encoded = encodeURIComponent(friendlySafeFilename(filename, null, true));
|
||||
return `attachment; filename*=UTF-8''${encoded}; filename="${encoded}"`;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user