1
0
mirror of https://github.com/laurent22/joplin.git synced 2026-01-29 07:46:13 +02:00

Compare commits

..

4 Commits

Author SHA1 Message Date
Laurent Cozic
d71ac2e218 tests 2021-09-18 11:27:26 +01:00
Laurent Cozic
c35c5a5821 ui 2021-09-17 20:15:43 +01:00
Laurent Cozic
bcb08ac8a2 Merge branch 'dev' into server_tasks 2021-09-17 18:28:43 +01:00
Laurent Cozic
6ff8d775c2 Add support for server tasks 2021-09-16 17:37:51 +01:00
34 changed files with 82 additions and 220 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 297 B

View File

@@ -137,8 +137,8 @@ fi
#-----------------------------------------------------
print 'Downloading Joplin...'
TEMP_DIR=$(mktemp -d)
wget -O "${TEMP_DIR}/Joplin.AppImage" "https://github.com/laurent22/joplin/releases/download/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage"
wget -O "${TEMP_DIR}/joplin.png" https://joplinapp.org/images/Icon512.png
wget -O ${TEMP_DIR}/Joplin.AppImage https://github.com/laurent22/joplin/releases/download/v${RELEASE_VERSION}/Joplin-${RELEASE_VERSION}.AppImage
wget -O ${TEMP_DIR}/joplin.png https://joplinapp.org/images/Icon512.png
#-----------------------------------------------------
print 'Installing Joplin...'
@@ -149,7 +149,7 @@ rm -f ~/.joplin/*.AppImage ~/.local/share/applications/joplin.desktop ~/.joplin/
mkdir -p ~/.joplin/
# Download the latest version
mv "${TEMP_DIR}/Joplin.AppImage" ~/.joplin/Joplin.AppImage
mv ${TEMP_DIR}/Joplin.AppImage ~/.joplin/Joplin.AppImage
# Gives execution privileges
chmod +x ~/.joplin/Joplin.AppImage
@@ -159,7 +159,7 @@ print "${COLOR_GREEN}OK${COLOR_RESET}"
#-----------------------------------------------------
print 'Installing icon...'
mkdir -p ~/.local/share/icons/hicolor/512x512/apps
mv "${TEMP_DIR}/joplin.png" ~/.local/share/icons/hicolor/512x512/apps/joplin.png
mv ${TEMP_DIR}/joplin.png ~/.local/share/icons/hicolor/512x512/apps/joplin.png
print "${COLOR_GREEN}OK${COLOR_RESET}"
# Detect desktop environment
@@ -222,7 +222,7 @@ fi
print "${COLOR_GREEN}Joplin version${COLOR_RESET} ${RELEASE_VERSION} ${COLOR_GREEN}installed.${COLOR_RESET}"
# Record version
echo "$RELEASE_VERSION" > ~/.joplin/VERSION
echo $RELEASE_VERSION > ~/.joplin/VERSION
#-----------------------------------------------------
if [[ "$SHOW_CHANGELOG" == true ]]; then
@@ -232,5 +232,5 @@ fi
#-----------------------------------------------------
print "Cleaning up..."
rm -rf "$TEMP_DIR"
rm -rf $TEMP_DIR
print "${COLOR_GREEN}OK${COLOR_RESET}"

View File

@@ -84,10 +84,4 @@ describe('HtmlToMd', function() {
}
}));
it('should allow disabling escape', async () => {
const htmlToMd = new HtmlToMd();
expect(htmlToMd.parse('https://test.com/1_2_3.pdf', { disableEscapeContent: true })).toBe('https://test.com/1_2_3.pdf');
expect(htmlToMd.parse('https://test.com/1_2_3.pdf', { disableEscapeContent: false })).toBe('https://test.com/1\\_2\\_3.pdf');
});
});

View File

@@ -1 +1 @@
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> <span style="text-decoration: underline;">Insert alt</span> <s>Strike</s>
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> <s>Strike</s>

View File

@@ -1 +1 @@
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> <ins>Insert alt</ins> ~~Strike~~
X<sub>1</sub> X<sup>1</sup> <ins>Insert</ins> ~~Strike~~

View File

@@ -19,11 +19,6 @@ export const runtime = (): CommandRuntime => {
visible: !layoutItemProp(layout, 'noteList', 'visible'),
});
// Toggling the sidebar will affect the size of most other on-screen components.
// Dispatching a window resize event is a bit of a hack, but it ensures that any
// component that watches for resizes will be accurately notified
window.dispatchEvent(new Event('resize'));
context.dispatch({
type: 'MAIN_LAYOUT_SET',
value: newLayout,

View File

@@ -19,11 +19,6 @@ export const runtime = (): CommandRuntime => {
visible: !layoutItemProp(layout, 'sideBar', 'visible'),
});
// Toggling the sidebar will affect the size of most other on-screen components.
// Dispatching a window resize event is a bit of a hack, but it ensures that any
// component that watches for resizes will be accurately notified
window.dispatchEvent(new Event('resize'));
context.dispatch({
type: 'MAIN_LAYOUT_SET',
value: newLayout,

View File

@@ -6,8 +6,7 @@ import { EditorCommand, NoteBodyEditorProps } from '../../utils/types';
import { commandAttachFileToBody, handlePasteEvent } from '../../utils/resourceHandling';
import { ScrollOptions, ScrollOptionTypes } from '../../utils/types';
import { CommandValue } from '../../utils/types';
import { useScrollHandler, usePrevious, cursorPositionToTextOffset } from './utils';
import useElementSize from '@joplin/lib/hooks/useElementSize';
import { useScrollHandler, usePrevious, cursorPositionToTextOffset, useRootSize } from './utils';
import Toolbar from './Toolbar';
import styles_ from './styles';
import { RenderedBody, defaultRenderedBody } from './utils/types';
@@ -60,7 +59,7 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
const props_onChangeRef = useRef<Function>(null);
props_onChangeRef.current = props.onChange;
const rootSize = useElementSize(rootRef);
const rootSize = useRootSize({ rootRef });
usePluginServiceRegistration(ref);

View File

@@ -1,4 +1,4 @@
import { useEffect, useCallback, useRef } from 'react';
import { useEffect, useCallback, useRef, useState } from 'react';
import shim from '@joplin/lib/shim';
export function cursorPositionToTextOffset(cursorPos: any, body: string) {
@@ -89,3 +89,21 @@ export function useScrollHandler(editorRef: any, webviewRef: any, onScroll: Func
return { resetScroll, setEditorPercentScroll, setViewerPercentScroll, editor_scroll };
}
export function useRootSize(dependencies: any) {
const { rootRef } = dependencies;
const [rootSize, setRootSize] = useState({ width: 0, height: 0 });
useEffect(() => {
if (!rootRef.current) return;
const { width, height } = rootRef.current.getBoundingClientRect();
if (rootSize.width !== width || rootSize.height !== height) {
setRootSize({ width: width, height: height });
}
});
return rootSize;
}

View File

@@ -171,7 +171,6 @@ export default function useKeymap(CodeMirror: any) {
'Cmd-Right': 'goLineRightSmart',
'Alt-Backspace': 'delGroupBefore',
'Alt-Delete': 'delGroupAfter',
'Cmd-Backspace': 'delWrappedLineLeft',
'fallthrough': 'basic',
};

View File

@@ -793,7 +793,7 @@ const TinyMCE = (props: NoteBodyEditorProps, ref: any) => {
};
}
await loadDocumentAssets(editor, await props.allAssets(props.contentMarkupLanguage, { contentMaxWidthTarget: '.mce-content-body' }));
await loadDocumentAssets(editor, await props.allAssets(props.contentMarkupLanguage));
dispatchDidUpdate(editor);
};

View File

@@ -14,7 +14,7 @@ import useMarkupToHtml from './utils/useMarkupToHtml';
import useFormNote, { OnLoadEvent } from './utils/useFormNote';
import useFolder from './utils/useFolder';
import styles_ from './styles';
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps, AllAssetsOptions } from './utils/types';
import { NoteEditorProps, FormNote, ScrollOptions, ScrollOptionTypes, OnChangeEvent, NoteBodyEditorProps } from './utils/types';
import ResourceEditWatcher from '@joplin/lib/services/ResourceEditWatcher/index';
import CommandService from '@joplin/lib/services/CommandService';
import ToolbarButton from '../ToolbarButton/ToolbarButton';
@@ -151,7 +151,7 @@ function NoteEditor(props: NoteEditorProps) {
plugins: props.plugins,
});
const allAssets = useCallback(async (markupLanguage: number, options: AllAssetsOptions = null): Promise<any[]> => {
const allAssets = useCallback(async (markupLanguage: number): Promise<any[]> => {
const theme = themeStyle(props.themeId);
const markupToHtml = markupLanguageUtils.newMarkupToHtml({}, {
@@ -159,10 +159,7 @@ function NoteEditor(props: NoteEditorProps) {
customCss: props.customCss,
});
return markupToHtml.allAssets(markupLanguage, theme, {
contentMaxWidth: props.contentMaxWidth,
contentMaxWidthTarget: options.contentMaxWidthTarget,
});
return markupToHtml.allAssets(markupLanguage, theme, { contentMaxWidth: props.contentMaxWidth });
}, [props.themeId, props.customCss, props.contentMaxWidth]);
const handleProvisionalFlag = useCallback(() => {

View File

@@ -69,17 +69,8 @@ export function htmlToClipboardData(html: string): ClipboardData {
// In that case we need to set both HTML and Text context, otherwise it
// won't be possible to paste the text in, for example, a text editor.
// https://github.com/laurent22/joplin/issues/4788
//
// Also we don't escape the content produced in HTML to MD conversion
// because it's not what would be expected. For example, if the content is
// `* something`, strictly speaking it would be correct to escape to `\*
// something`, however this is not what the user would expect when copying
// text. Likewise for URLs that contain "_". So the resulting Markdown might
// not be perfectly valid but would be closer to what a user would expect.
// If they want accurate MArkdown they can switch to the MD editor.
// https://github.com/laurent22/joplin/issues/5440
return {
text: htmlToMd().parse(copyableContent, { disableEscapeContent: true }),
text: htmlToMd().parse(copyableContent),
html: cleanUpCodeBlocks(copyableContent),
};
}

View File

@@ -6,10 +6,6 @@ import { MarkupLanguage } from '@joplin/renderer';
import { RenderResult, RenderResultPluginAsset } from '@joplin/renderer/MarkupToHtml';
import { MarkupToHtmlOptions } from './useMarkupToHtml';
export interface AllAssetsOptions {
contentMaxWidthTarget?: string;
}
export interface ToolbarButtonInfos {
[key: string]: ToolbarButtonInfo;
}
@@ -59,7 +55,7 @@ export interface NoteBodyEditorProps {
onScroll(event: any): void;
markupToHtml: (markupLanguage: MarkupLanguage, markup: string, options: MarkupToHtmlOptions)=> Promise<RenderResult>;
htmlToMarkdown: Function;
allAssets: (markupLanguage: MarkupLanguage, options: AllAssetsOptions)=> Promise<RenderResultPluginAsset[]>;
allAssets: (markupLanguage: MarkupLanguage)=> Promise<RenderResultPluginAsset[]>;
disabled: boolean;
dispatch: Function;
noteToolbar: any;

View File

@@ -139,10 +139,9 @@ class NoteSearchBarComponent extends React.Component {
color: theme.colorFaded,
backgroundColor: theme.backgroundColor,
});
const matchesFoundString = (query.length > 0) ? (
const matchesFoundString = (query.length > 0 && this.props.resultCount > 0) ? (
<div style={textStyle}>
{`${this.props.resultCount === 0 ? 0 : this.props.selectedIndex + 1} / ${this.props.resultCount}`}
{`${this.props.selectedIndex + 1} / ${this.props.resultCount}`}
</div>
) : null;

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/app-desktop",
"version": "2.4.7",
"version": "2.4.6",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/app-desktop",
"version": "2.4.7",
"version": "2.4.6",
"license": "MIT",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.13.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/app-desktop",
"version": "2.4.7",
"version": "2.4.6",
"description": "Joplin for Desktop",
"main": "main.js",
"private": true,

View File

@@ -251,12 +251,6 @@ export default class BaseApplication {
continue;
}
if (arg.indexOf('--user-data-dir=') === 0) {
// Electron-specific flag. Allows users to run the app with chromedriver.
argv.splice(0, 1);
continue;
}
if (arg.length && arg[0] == '-') {
throw new JoplinError(_('Unknown flag: %s', arg), 'flagError');
} else {

View File

@@ -6,7 +6,6 @@ export interface ParseOptions {
anchorNames?: string[];
preserveImageTagsWithSize?: boolean;
baseUrl?: string;
disableEscapeContent?: boolean;
}
export default class HtmlToMd {
@@ -21,7 +20,6 @@ export default class HtmlToMd {
emDelimiter: '*',
strongDelimiter: '**',
br: '',
disableEscapeContent: 'disableEscapeContent' in options ? options.disableEscapeContent : false,
});
turndown.use(turndownPluginGfm);
turndown.remove('script');

View File

@@ -479,31 +479,6 @@ export default class Synchronizer {
void this.cancel();
});
// ========================================================================
// 2. DELETE_REMOTE
// ------------------------------------------------------------------------
// Delete the remote items that have been deleted locally.
// ========================================================================
if (syncSteps.indexOf('delete_remote') >= 0) {
const deletedItems = await BaseItem.deletedItems(syncTargetId);
for (let i = 0; i < deletedItems.length; i++) {
if (this.cancelling()) break;
const item = deletedItems[i];
const path = BaseItem.systemPath(item.item_id);
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
await this.apiCall('delete', path);
if (item.item_type === BaseModel.TYPE_RESOURCE) {
const remoteContentPath = resourceRemotePath(item.item_id);
await this.apiCall('delete', remoteContentPath);
}
await BaseItem.remoteDeletedItem(syncTargetId, item.item_id);
}
} // DELETE_REMOTE STEP
// ========================================================================
// 1. UPLOAD
// ------------------------------------------------------------------------
@@ -788,6 +763,31 @@ export default class Synchronizer {
}
} // UPLOAD STEP
// ========================================================================
// 2. DELETE_REMOTE
// ------------------------------------------------------------------------
// Delete the remote items that have been deleted locally.
// ========================================================================
if (syncSteps.indexOf('delete_remote') >= 0) {
const deletedItems = await BaseItem.deletedItems(syncTargetId);
for (let i = 0; i < deletedItems.length; i++) {
if (this.cancelling()) break;
const item = deletedItems[i];
const path = BaseItem.systemPath(item.item_id);
this.logSyncOperation('deleteRemote', null, { id: item.item_id }, 'local has been deleted');
await this.apiCall('delete', path);
if (item.item_type === BaseModel.TYPE_RESOURCE) {
const remoteContentPath = resourceRemotePath(item.item_id);
await this.apiCall('delete', remoteContentPath);
}
await BaseItem.remoteDeletedItem(syncTargetId, item.item_id);
}
} // DELETE_REMOTE STEP
// ------------------------------------------------------------------------
// 3. DELTA
// ------------------------------------------------------------------------

View File

@@ -386,34 +386,6 @@ describe('services_SearchFilter', function() {
expect(ids(rows).sort()).toEqual(ids(notes0).concat(ids(notes00).concat(ids(notes1))).sort());
}));
it('should support filtering and search term', (async () => {
const notebook1 = await Folder.save({ title: 'notebook1' });
const notebook2 = await Folder.save({ title: 'notebook2' });
const note1 = await Note.save({ title: 'note1', body: 'abcdefg', parent_id: notebook1.id });
await Note.save({ title: 'note2', body: 'body', parent_id: notebook1.id });
await Note.save({ title: 'note3', body: 'abcdefg', parent_id: notebook2.id });
await Note.save({ title: 'note4', body: 'body', parent_id: notebook2.id });
await engine.syncTables();
const testCases = [
{ searchQuery: 'notebook:notebook1 abcdefg' },
{ searchQuery: 'notebook:notebook1 "abcdefg"' },
{ searchQuery: 'notebook:"notebook1" abcdefg' },
{ searchQuery: 'notebook:"notebook1" "abcdefg"' },
{ searchQuery: 'notebook:"notebook1" -tag:* "abcdefg"' },
{ searchQuery: 'notebook:"notebook1" -tag:* abcdefg' },
{ searchQuery: 'notebook:"notebook1" -tag:"*" abcdefg' },
{ searchQuery: 'notebook:"notebook1" -tag:"*" "abcdefg"' },
];
for (const testCase of testCases) {
const rows = await engine.search(testCase.searchQuery, { searchType });
expect(rows.length).toBe(1);
expect(ids(rows)).toContain(note1.id);
}
}));
it('should support filtering by created date', (async () => {
let rows;
const n1 = await Note.save({ title: 'I made this on', body: 'May 20 2020', user_created_time: Date.parse('2020-05-20') });

View File

@@ -36,7 +36,6 @@ const getTerms = (query: string, validFilters: Set<string>): Term[] => {
if (inQuote) {
terms.push(makeTerm(currentCol, currentTerm));
currentTerm = '';
currentCol = '_';
inQuote = false;
} else {
inQuote = true;

View File

@@ -9,7 +9,6 @@ function formatCssSize(v: any): string {
export interface Options {
contentMaxWidth?: number;
contentMaxWidthTarget?: string;
}
export default function(theme: any, options: Options = null) {
@@ -22,9 +21,8 @@ export default function(theme: any, options: Options = null) {
const fontFamily = '\'Avenir\', \'Arial\', sans-serif';
const maxWidthTarget = options.contentMaxWidthTarget ? options.contentMaxWidthTarget : '#rendered-md';
const maxWidthCss = options.contentMaxWidth ? `
${maxWidthTarget} {
#rendered-md {
max-width: ${options.contentMaxWidth}px;
margin-left: auto;
margin-right: auto;

View File

@@ -1,12 +1,12 @@
{
"name": "@joplin/server",
"version": "2.4.8",
"version": "2.4.7",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@joplin/server",
"version": "2.4.8",
"version": "2.4.7",
"dependencies": {
"@fortawesome/fontawesome-free": "^5.15.1",
"@koa/cors": "^3.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@joplin/server",
"version": "2.4.8",
"version": "2.4.7",
"private": true,
"scripts": {
"start-dev": "nodemon --config nodemon.json --ext ts,js,mustache,css,tsx dist/app.js --env dev",

View File

@@ -196,34 +196,6 @@ describe('ItemModel', function() {
expect((await models().user().load(user3.id)).total_item_size).toBe(totalSize3);
});
test('should update total size when an item is deleted', async function() {
const { user: user1 } = await createUserAndSession(1);
await createItemTree3(user1.id, '', '', [
{
id: '000000000000000000000000000000F1',
children: [
{
id: '00000000000000000000000000000001',
},
],
},
]);
const folder1 = await models().item().loadByJopId(user1.id, '000000000000000000000000000000F1');
const note1 = await models().item().loadByJopId(user1.id, '00000000000000000000000000000001');
await models().item().updateTotalSizes();
expect((await models().user().load(user1.id)).total_item_size).toBe(folder1.content_size + note1.content_size);
await models().item().delete(note1.id);
await models().item().updateTotalSizes();
expect((await models().user().load(user1.id)).total_item_size).toBe(folder1.content_size);
});
test('should include shared items in total size calculation', async function() {
const { user: user1, session: session1 } = await createUserAndSession(1);
const { user: user2, session: session2 } = await createUserAndSession(2);

View File

@@ -623,11 +623,7 @@ export default class ItemModel extends BaseModel<Item> {
} else {
const itemIds: Uuid[] = unique(changes.map(c => c.item_id));
const userItems: UserItem[] = await this.db('user_items').select('user_id').whereIn('item_id', itemIds);
const userIds: Uuid[] = unique(
userItems
.map(u => u.user_id)
.concat(changes.map(c => c.user_id))
);
const userIds: Uuid[] = unique(userItems.map(u => u.user_id));
const totalSizes: TotalSizeRow[] = [];
for (const userId of userIds) {

View File

@@ -180,7 +180,7 @@ export default class UserModel extends BaseModel<User> {
}
public async checkMaxItemSizeLimit(user: User, buffer: Buffer, item: Item, joplinItem: any) {
// If the item is encrypted, we apply a multiplier because encrypted
// If the item is encrypted, we apply a multipler because encrypted
// items can be much larger (seems to be up to twice the size but for
// safety let's go with 2.2).
@@ -198,20 +198,14 @@ export default class UserModel extends BaseModel<User> {
));
}
// We allow lock files to go through so that sync can happen, which in
// turns allow user to fix oversized account by deleting items.
const isWhiteListed = itemSize < 200 && item.name.startsWith('locks/');
if (!isWhiteListed) {
// Also apply a multiplier to take into account E2EE overhead
const maxTotalItemSize = getMaxTotalItemSize(user) * 1.5;
if (maxTotalItemSize && user.total_item_size + itemSize >= maxTotalItemSize) {
throw new ErrorPayloadTooLarge(_('Cannot save %s "%s" because it would go over the total allowed size (%s) for this account',
isNote ? _('note') : _('attachment'),
itemTitle ? itemTitle : item.name,
formatBytes(maxTotalItemSize)
));
}
// Also apply a multiplier to take into account E2EE overhead
const maxTotalItemSize = getMaxTotalItemSize(user) * 1.5;
if (maxTotalItemSize && user.total_item_size + itemSize >= maxTotalItemSize) {
throw new ErrorPayloadTooLarge(_('Cannot save %s "%s" because it would go over the total allowed size (%s) for this account',
isNote ? _('note') : _('attachment'),
itemTitle ? itemTitle : item.name,
formatBytes(maxTotalItemSize)
));
}
}

View File

@@ -5,7 +5,7 @@ import Router from '../../utils/Router';
import { RouteType } from '../../utils/types';
import { AppContext } from '../../utils/types';
import * as fs from 'fs-extra';
import { ErrorForbidden, ErrorMethodNotAllowed, ErrorNotFound, ErrorPayloadTooLarge, errorToPlainObject } from '../../utils/errors';
import { ErrorForbidden, ErrorMethodNotAllowed, ErrorNotFound, ErrorPayloadTooLarge } from '../../utils/errors';
import ItemModel, { ItemSaveOption, SaveFromRawContentItem } from '../../models/ItemModel';
import { requestDeltaPagination, requestPagination } from '../../models/utils/pagination';
import { AclAction } from '../../models/BaseModel';
@@ -66,9 +66,7 @@ export async function putItemContents(path: SubPath, ctx: AppContext, isBatch: b
const output = await ctx.joplin.models.item().saveFromRawContent(ctx.joplin.owner, items, saveOptions);
for (const [name] of Object.entries(output)) {
if (output[name].item) output[name].item = ctx.joplin.models.item().toApiOutput(output[name].item) as Item;
if (output[name].error) output[name].error = errorToPlainObject(output[name].error);
}
return output;
}

View File

@@ -114,17 +114,3 @@ export function errorToString(error: Error): string {
if (error.stack) msg.push(error.stack);
return msg.join(': ');
}
interface PlainObjectError {
httpCode?: number;
message?: string;
code?: string;
}
export function errorToPlainObject(error: any): PlainObjectError {
const output: PlainObjectError = {};
if ('httpCode' in error) output.httpCode = error.httpCode;
if ('code' in error) output.code = error.code;
if ('message' in error) output.message = error.message;
return output;
}

View File

@@ -1,4 +1,4 @@
import { repeat, isCodeBlockSpecialCase1, isCodeBlockSpecialCase2, isCodeBlock, getStyleProp } from './utilities'
import { repeat, isCodeBlockSpecialCase1, isCodeBlockSpecialCase2, isCodeBlock } from './utilities'
const Entities = require('html-entities').AllHtmlEntities;
const htmlentities = (new Entities()).encode;
@@ -73,15 +73,7 @@ rules.highlight = {
// HTML to avoid any ambiguity.
rules.insert = {
filter: function (node, options) {
// TinyMCE represents this either with an <INS> tag (when pressing the
// toolbar button) or using style "text-decoration" (when using shortcut
// Cmd+U)
//
// https://github.com/laurent22/joplin/issues/5480
if (node.nodeName === 'INS') return true;
return getStyleProp(node, 'text-decoration') === 'underline';
},
filter: 'ins',
replacement: function (content, node, options) {
return '<ins>' + content + '</ins>'

View File

@@ -23,7 +23,6 @@ export default function TurndownService (options) {
linkReferenceStyle: 'full',
anchorNames: [],
br: ' ',
disableEscapeContent: false,
blankReplacement: function (content, node) {
return node.isBlock ? '\n\n' : ''
},
@@ -182,8 +181,6 @@ TurndownService.prototype = {
*/
function process (parentNode, escapeContent = 'auto') {
if (this.options.disableEscapeContent) escapeContent = false;
var self = this
return reduce.call(parentNode.childNodes, function (output, node) {
node = new Node(node)

View File

@@ -78,16 +78,3 @@ export function isCodeBlock(node) {
node.firstChild.nodeName === 'CODE'
)
}
export function getStyleProp(node, name) {
const style = node.getAttribute('style');
if (!style) return null;
name = name.toLowerCase();
if (!style.toLowerCase().includes(name)) return null;
const o = css.parse('div {' + style + '}');
if (!o.stylesheet.rules.length) return null;
const prop = o.stylesheet.rules[0].declarations.find(d => d.property.toLowerCase() === name);
return prop ? prop.value : null;
}

View File

@@ -1,9 +1,5 @@
# Joplin Server Changelog
## [server-v2.4.8-beta](https://github.com/laurent22/joplin/releases/tag/server-v2.4.8-beta) (Pre-release) - 2021-09-15T22:16:59Z
- New: Added support for app level slow SQL query log (5e8b742)
## [server-v2.4.7-beta](https://github.com/laurent22/joplin/releases/tag/server-v2.4.7-beta) (Pre-release) - 2021-09-15T15:58:46Z
- Improved: Improve flag logic (c229821)