diff --git a/.editorconfig b/.editorconfig index 7453446..5044f66 100644 --- a/.editorconfig +++ b/.editorconfig @@ -18,3 +18,10 @@ indent_size = 4 [*.yml] indent_style = space indent_size = 2 + +[*.js] +ij_javascript_spaces_within_object_literal_braces = true +ij_javascript_keep_simple_blocks_in_one_line = true +ij_javascript_keep_simple_methods_in_one_line = true +ij_javascript_spaces_within_imports = true + diff --git a/src/commonmark/commonmark.js b/src/commonmark/commonmark.js index 4c3d718..74c4bbf 100644 --- a/src/commonmark/commonmark.js +++ b/src/commonmark/commonmark.js @@ -7,6 +7,6 @@ import CommonMarkDataProcessor from './commonmarkdataprocessor'; // Simple plugin which loads the data processor. export default function CommonMarkPlugin(editor) { - editor.data.processor = new CommonMarkDataProcessor(editor.editing.view.document); + editor.data.processor = new CommonMarkDataProcessor(editor); } diff --git a/src/commonmark/commonmarkdataprocessor.js b/src/commonmark/commonmarkdataprocessor.js index 3ed3388..0455285 100644 --- a/src/commonmark/commonmarkdataprocessor.js +++ b/src/commonmark/commonmarkdataprocessor.js @@ -9,16 +9,17 @@ /* eslint-env browser */ -import {HtmlDataProcessor, ViewDomConverter} from '@ckeditor/ckeditor5-engine'; -import {highlightedCodeBlock} from 'turndown-plugin-gfm'; +import { HtmlDataProcessor, ViewDomConverter } from '@ckeditor/ckeditor5-engine'; +import { highlightedCodeBlock } from 'turndown-plugin-gfm'; import TurndownService from 'turndown'; -import {textNodesPreprocessor, linkPreprocessor, breaksPreprocessor} from './utils/preprocessor'; -import {fixTasklistWhitespaces} from './utils/fix-tasklist-whitespaces'; -import {hoistTaskListCheckboxes} from './utils/hoist-task-list-checkboxes'; -import {fixBreaksInTables, fixBreaksInLists, fixBreaksOnRootLevel} from "./utils/fix-breaks"; +import { textNodesPreprocessor, linkPreprocessor, breaksPreprocessor } from './utils/preprocessor'; +import { fixTasklistWhitespaces } from './utils/fix-tasklist-whitespaces'; +import { hoistTaskListCheckboxes } from './utils/hoist-task-list-checkboxes'; +import { fixBreaksInTables, fixBreaksInLists, fixBreaksOnRootLevel } from "./utils/fix-breaks"; import markdownIt from 'markdown-it'; import markdownItTaskLists from 'markdown-it-task-lists'; -import {isPageBreakNode, PAGE_BREAK_MARKDOWN} from "./utils/page-breaks"; +import { isPageBreakNode, PAGE_BREAK_MARKDOWN } from "./utils/page-breaks"; +import { getOPPath } from "../plugins/op-context/op-context"; export const originalSrcAttribute = 'data-original-src'; @@ -54,15 +55,51 @@ function workPackageRefInlineRule(state, silent) { return true; } +const WIKI_PAGE_LINK_RE = /(?:\\\[){3}([0-9]+):([^\\\n]+)(?:\\\]){3}/; + +function wikiPageLinkInlineRule(state, silent, editor) { + const start = state.pos; + const src = state.src; + + if (src.charCodeAt(start) !== 0x5c /* \ */) return false; + + const word = src.slice(start); + const match = WIKI_PAGE_LINK_RE.exec(word); + if (!match) return false; + if (silent) return true; + + const providerId = match[1]; + const pageIdentifier = match[2]; + + const frameId = crypto.randomUUID(); + const frameSrc = getOPPath(editor).wikiPageLinkMacro(providerId, pageIdentifier, frameId) + + const html = ` +` + + const token = state.push('html_inline', '', 0); + token.content = html; + state.pos = start + match[0].length; + return true; +} + /** * This data processor implementation uses CommonMark as input/output data. * * @implements module:engine/dataprocessor/dataprocessor~DataProcessor */ export default class CommonMarkDataProcessor { - constructor(document) { + constructor(editor) { + const document = editor.editing.view.document; this._htmlDP = new HtmlDataProcessor(document); this._domConverter = new ViewDomConverter(document); + this.editor = editor; } /** @@ -81,9 +118,14 @@ export default class CommonMarkDataProcessor { }); // Use tasklist plugin - let parser = md.use(markdownItTaskLists, {label: true}); + let parser = md.use(markdownItTaskLists, { label: true }); parser.inline.ruler.before('text', 'op_workpackage_ref', workPackageRefInlineRule); + parser.inline.ruler.before( + 'text', + 'op_wiki_page_link', + (state, silent) => wikiPageLinkInlineRule(state, silent, this.editor) + ); const previousRenderer = parser.renderer.rules.code_block; md.renderer.rules.code_block = function (tokens, idx, options, env, self) { @@ -207,7 +249,7 @@ export default class CommonMarkDataProcessor { * @see */ turndownService.addRule('orderedListItems', { - filter: function(node) { + filter: function (node) { if (node.nodeName !== 'LI') { return false; } @@ -215,28 +257,28 @@ export default class CommonMarkDataProcessor { return !!node.closest('ol'); }, replacement: function (content, node, options) { - content = content - .replace(/^\n+/, '') // remove leading newlines - .replace(/\n+$/, '\n'); // replace trailing newlines with just a single one - - var parent = node.parentNode; - var prefix = options.bulletListMarker + ' '; - var number = 1; - if (parent.nodeName === 'OL') { - var start = parent.getAttribute('start'); - var index = Array.prototype.indexOf.call(parent.children, node); - number = start ? Number(start) + index : index + 1; - prefix = number + '. '; - } - - // Calculate indentation based on the width of the number prefix - var indentWidth = prefix.length; - var indent = ' '.repeat(indentWidth); - content = content.replace(/\n/gm, '\n' + indent); - - return ( - prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '') - ); + content = content + .replace(/^\n+/, '') // remove leading newlines + .replace(/\n+$/, '\n'); // replace trailing newlines with just a single one + + var parent = node.parentNode; + var prefix = options.bulletListMarker + ' '; + var number = 1; + if (parent.nodeName === 'OL') { + var start = parent.getAttribute('start'); + var index = Array.prototype.indexOf.call(parent.children, node); + number = start ? Number(start) + index : index + 1; + prefix = number + '. '; + } + + // Calculate indentation based on the width of the number prefix + var indentWidth = prefix.length; + var indent = ' '.repeat(indentWidth); + content = content.replace(/\n/gm, '\n' + indent); + + return ( + prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '') + ); } }); @@ -280,10 +322,10 @@ export default class CommonMarkDataProcessor { replacement: function (_content, node) { // Remove filler attribute, but keep empty lines node.querySelectorAll('td br[data-cke-filler]').forEach((node) => { - if (node.nextElementSibling) { - node.removeAttribute('data-cke-filler'); - } - }); + if (node.nextElementSibling) { + node.removeAttribute('data-cke-filler'); + } + }); return node.outerHTML; } @@ -306,6 +348,15 @@ export default class CommonMarkDataProcessor { }, }); + turndownService.addRule('wikiPageLink', { + filter: (node) => node.nodeName === 'TURBO-FRAME' && node.getAttribute('data-type') === 'wiki-page-link', + replacement: (_content, node) => { + const providerId = node.getAttribute('data-provider-id') || ''; + const pageIdentifier = node.getAttribute('data-page-identifier') || ''; + return `\\[\\[\\[${providerId}:${pageIdentifier}\\]\\]\\]`; + }, + }); + turndownService.addRule('openProjectMacros', { filter: ['macro'], replacement: (_content, node) => { diff --git a/src/op-plugins.js b/src/op-plugins.js index 8d7453b..8cf7c69 100644 --- a/src/op-plugins.js +++ b/src/op-plugins.js @@ -43,6 +43,7 @@ import { PageBreak } from '@ckeditor/ckeditor5-page-break'; import { Autosave } from '@ckeditor/ckeditor5-autosave'; import OpContentRevisions from "./plugins/op-content-revisions/op-content-revisions"; import OPMacroWpQuickinfoPlugin from "./plugins/op-macro-wp-quickinfo/op-macro-wp-quickinfo-plugin"; +import OpMacroWikiPageLinkPlugin from "./plugins/op-macro-wiki-page-link/op-macro-wiki-page-link-plugin"; // We divide our plugins into separate concerns here // in order to enable / disable each group by configuration @@ -93,6 +94,7 @@ export const builtinPlugins = [ OPSourceCodePlugin, OpContentRevisions, OPMacroWpQuickinfoPlugin, + OpMacroWikiPageLinkPlugin, CodeBlockPlugin, CommonMark, diff --git a/src/plugins/op-macro-wiki-page-link/op-macro-wiki-page-link-plugin.js b/src/plugins/op-macro-wiki-page-link/op-macro-wiki-page-link-plugin.js new file mode 100644 index 0000000..e200b4b --- /dev/null +++ b/src/plugins/op-macro-wiki-page-link/op-macro-wiki-page-link-plugin.js @@ -0,0 +1,87 @@ +import { Plugin } from "@ckeditor/ckeditor5-core"; +import { toWidget } from "@ckeditor/ckeditor5-widget"; +import { getOPPath } from "../op-context/op-context"; + +const MODEL_ELEMENT_NAME = 'op-macro-wiki-page-link'; + +export default class OpMacroWikiPageLinkPlugin extends Plugin { + static get pluginName() { + return 'OpMacroWikiPageLink'; + } + + init() { + const editor = this.editor; + const conversion = editor.conversion; + + editor.model.schema.register(MODEL_ELEMENT_NAME, { + allowWhere: '$text', + isInline: true, + isObject: true, + allowAttributes: ['providerId', 'pageIdentifier'], + }); + + conversion.for('upcast').elementToElement({ + view: { name: 'turbo-frame', attributes: { 'data-type': 'wiki-page-link' } }, + model: (viewElement, { writer }) => { + const providerId = viewElement.getAttribute('data-provider-id'); + const pageIdentifier = viewElement.getAttribute('data-page-identifier'); + return writer.createElement(MODEL_ELEMENT_NAME, { providerId, pageIdentifier }); + }, + }); + + conversion.for('dataDowncast').elementToElement({ + model: MODEL_ELEMENT_NAME, + view: (modelElement, { writer }) => { + const providerId = modelElement.getAttribute('providerId'); + const pageIdentifier = modelElement.getAttribute('pageIdentifier'); + const frameId = crypto.randomUUID(); + + const container = writer.createContainerElement( + 'turbo-frame', + { + id: frameId, + src: this.macroUrl(providerId, pageIdentifier, frameId), + 'data-provider-id': providerId, + 'data-page-identifier': pageIdentifier, + 'data-type': 'wiki-page-link', + }); + + const ref = `[[[${providerId}:${pageIdentifier}]]]`; + writer.insert(writer.createPositionAt(container, 0), writer.createText(ref)); + return container; + }, + }); + + conversion.for('editingDowncast').elementToElement({ + model: MODEL_ELEMENT_NAME, + view: (modelElement, { writer }) => { + const providerId = modelElement.getAttribute('providerId'); + const pageIdentifier = modelElement.getAttribute('pageIdentifier'); + const frameId = crypto.randomUUID(); + + const wrapper = writer.createContainerElement('span', { + class: 'my-wiki-page-link-widget', + }); + + const raw = writer.createRawElement( + 'turbo-frame', + { + id: frameId, + src: this.macroUrl(providerId, pageIdentifier, frameId), + 'data-provider-id': providerId, + 'data-page-identifier': pageIdentifier, + 'data-type': 'wiki-page-link', + }, + () => { }, + ); + + writer.insert(writer.createPositionAt(wrapper, 0), raw); + return toWidget(wrapper, writer, { label: `[[[${providerId}:${pageIdentifier}]]]` }); + } + }); + } + + macroUrl(providerId, pageIdentifier, frameId) { + return getOPPath(this.editor).wikiPageLinkMacro(providerId, pageIdentifier, frameId) + } +} diff --git a/tests/commonmark/_utils/utils.js b/tests/commonmark/_utils/utils.js index eea7dd5..7accc2a 100644 --- a/tests/commonmark/_utils/utils.js +++ b/tests/commonmark/_utils/utils.js @@ -3,9 +3,10 @@ * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license */ +import { _stringifyView as stringify } from "@ckeditor/ckeditor5-engine"; +import { EditingView, StylesProcessor } from "@ckeditor/ckeditor5-engine"; + import MarkdownDataProcessor from '../../../src/commonmark/commonmarkdataprocessor'; -import {_stringifyView as stringify} from "@ckeditor/ckeditor5-engine"; -import {StylesProcessor, ViewDocument} from "@ckeditor/ckeditor5-engine"; /** * Tests MarkdownDataProcessor. @@ -20,9 +21,9 @@ import {StylesProcessor, ViewDocument} from "@ckeditor/ckeditor5-engine"; * @returns {module:engine/view/documentfragment~DocumentFragment} */ export function testDataProcessor(markdown, viewString, normalizedMarkdown, options) { - const viewDocument = new ViewDocument(new StylesProcessor()); + const editor = createTestEditor(); - const dataProcessor = new MarkdownDataProcessor(viewDocument); + const dataProcessor = new MarkdownDataProcessor(editor); if (options && options.setup) { options.setup(dataProcessor); @@ -46,6 +47,12 @@ export function testDataProcessor(markdown, viewString, normalizedMarkdown, opti return viewFragment; } +export function createTestEditor() { + const view = new EditingView(new StylesProcessor()); + + return { editing: { view } }; +} + function cleanHtml(html) { // Space between table elements. html = html.replace(/(th|td|tr)>\s+<(\/?(?:th|td|tr))/g, '$1><$2'); diff --git a/tests/commonmark/escaping.test.js b/tests/commonmark/escaping.test.js index 0272067..7017005 100644 --- a/tests/commonmark/escaping.test.js +++ b/tests/commonmark/escaping.test.js @@ -4,25 +4,24 @@ */ import MarkdownDataProcessor from '../../src/commonmark/commonmarkdataprocessor'; -import {_stringifyView as stringify} from '@ckeditor/ckeditor5-engine'; -import {testDataProcessor} from './_utils/utils.js'; -import {StylesProcessor, ViewDocument} from "@ckeditor/ckeditor5-engine"; +import { _stringifyView as stringify } from '@ckeditor/ckeditor5-engine'; +import { testDataProcessor, createTestEditor } from './_utils/utils.js'; const testCases = { - 'backslash': {test: '\\\\', result: '\\'}, - 'underscore': {test: '\\_', result: '_'}, - 'left brace': {test: '\\{', result: '{'}, - 'right brace': {test: '\\}', result: '}'}, - 'left bracket': {test: '\\[', result: '['}, - 'right bracket': {test: '\\]', result: ']'}, - 'left paren': {test: '\\(', result: '('}, - 'right paren': {test: '\\)', result: ')'}, - 'greater than': {test: '\\>', result: '>'}, - 'hash': {test: '\\#', result: '#'}, - 'period': {test: '\\.', result: '.'}, - 'exclamation mark': {test: '\\!', result: '!'}, - 'plus': {test: '\\+', result: '+'}, - 'minus': {test: '\\-', result: '-'} + 'backslash': { test: '\\\\', result: '\\' }, + 'underscore': { test: '\\_', result: '_' }, + 'left brace': { test: '\\{', result: '{' }, + 'right brace': { test: '\\}', result: '}' }, + 'left bracket': { test: '\\[', result: '[' }, + 'right bracket': { test: '\\]', result: ']' }, + 'left paren': { test: '\\(', result: '(' }, + 'right paren': { test: '\\)', result: ')' }, + 'greater than': { test: '\\>', result: '>' }, + 'hash': { test: '\\#', result: '#' }, + 'period': { test: '\\.', result: '.' }, + 'exclamation mark': { test: '\\!', result: '!' }, + 'plus': { test: '\\+', result: '+' }, + 'minus': { test: '\\-', result: '-' } }; describe('Commonmark', () => { @@ -31,8 +30,7 @@ describe('Commonmark', () => { let dataProcessor; beforeEach(() => { - const viewDocument = new ViewDocument(new StylesProcessor()); - dataProcessor = new MarkdownDataProcessor(viewDocument); + dataProcessor = new MarkdownDataProcessor(createTestEditor()); }); for (const key in testCases) {