Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 85 additions & 17 deletions lib/migrator.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
'use strict';

const posts = require.main.require('./src/posts');

const MarkdownIt = require('markdown-it');

const markdown = new MarkdownIt();
const { QuillDeltaToHtmlConverter } = require('quill-delta-to-html');
const { TableParser } = require('quill-v1-table/TableDeltaToHtml');
const isHtml = require('is-html');

const winston = require.main.require('winston');

const Migrator = module.exports;

Migrator.resolveNbb = id => require.main.require(id);

Migrator.detect = (postObj) => {
const isHtml = Migrator.isHtml(postObj);

Expand All @@ -37,21 +36,90 @@ Migrator.isHtml = postObj => isHtml(postObj.content);

Migrator.isMarkdown = postObj => !Migrator.isHTML(postObj);

Migrator.toHtml = (content) => {
try {
content = JSON.parse(content);
const converter = new QuillDeltaToHtmlConverter(content.ops, {});

// Quill plugin should fire a hook here, passing converter.renderCustomWith
// Emoji plugin should take that method and register a listener.
// Also toHtml is probably going to end up being asynchronous, then... awaited?
converter.renderCustomWith((customOp) => {
if (customOp.insert.type === 'emoji') {
return `<img src="${customOp.insert.value.src}" alt="${customOp.attributes.alt}" class="${customOp.attributes.class}" />`;
function registerEmojiOnConverter(converter) {
converter.renderCustomWith((customOp) => {
if (customOp.insert.type === 'emoji') {
return `<img src="${customOp.insert.value.src}" alt="${customOp.attributes.alt}" class="${customOp.attributes.class}" />`;
}
});
}

function segmentDeltaOps(ops) {
const segments = [];
let i = 0;
while (i < ops.length) {
const op = ops[i];
const next = ops[i + 1];
const opTd = !!(op.attributes && op.attributes.td);
const nextTd = !!(next && next.attributes && next.attributes.td);

if (opTd || nextTd) {
const tableOps = [];
while (i < ops.length) {
const o = ops[i];
const n = ops[i + 1];
const oTd = !!(o.attributes && o.attributes.td);
const nTd = !!(n && n.attributes && n.attributes.td);
if (oTd || nTd) {
tableOps.push(o);
i += 1;
} else {
break;
}
}
segments.push({ type: 'table', ops: tableOps });
} else {
const textOps = [];
while (i < ops.length) {
const o = ops[i];
const n = ops[i + 1];
const oTd = !!(o.attributes && o.attributes.td);
const nTd = !!(n && n.attributes && n.attributes.td);
if (oTd || nTd) {
break;
}
textOps.push(o);
i += 1;
}
});
segments.push({ type: 'text', ops: textOps });
}
}
return segments;
}

return posts.sanitize(converter.convert());
Migrator.convertMixedDeltaToHtml = (delta) => {
const {ops} = delta;
if (!Array.isArray(ops)) {
return '';
}
const parts = [];
const segments = segmentDeltaOps(ops);
for (let s = 0; s < segments.length; s += 1) {
const seg = segments[s];
if (seg.type === 'table') {
const parser = new TableParser(seg.ops);
parser.parse();
parts.push(parser.toHTML());
} else if (seg.ops.length > 0) {
const converter = new QuillDeltaToHtmlConverter(seg.ops, {});
registerEmojiOnConverter(converter);
parts.push(converter.convert());
}
}
return parts.join('');
};

Migrator.toHtml = (content) => {
const posts = Migrator.resolveNbb('./src/posts');
const winston = Migrator.resolveNbb('winston');
try {
const delta = JSON.parse(content);
if (!delta || !Array.isArray(delta.ops)) {
winston.verbose('[plugin/composer-quill (toHtml)] Input not in expected format, skipping.');
return false;
}
const html = Migrator.convertMixedDeltaToHtml(delta);
return posts.sanitize(html);
} catch (e) {
// Do nothing
winston.verbose('[plugin/composer-quill (toHtml)] Input not in expected format, skipping.');
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nodebb-plugin-composer-quill",
"version": "4.1.0",
"version": "4.2.0",
"description": "Quill Composer for NodeBB",
"main": "library.js",
"repository": {
Expand Down Expand Up @@ -32,6 +32,7 @@
"node-quill-converter": "^0.3.2",
"quill": "^1.3.7",
"quill-delta-to-html": "^0.12.0",
"quill-v1-table": "^1.7.4",
"quill-magic-url": "^4.0.0",
"quill-markdown-shortcuts": "^0.0.10",
"screenfull": "^5.0.0"
Expand Down
3 changes: 2 additions & 1 deletion plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"quill.js": "./node_modules/quill/dist/quill.js",
"quill-magic-url.js": "./node_modules/quill-magic-url/dist/index.js",
"quill-markdown-shortcuts.js": "./node_modules/quill-markdown-shortcuts/dist/markdownShortcuts.js",
"quill-table.js": "./node_modules/quill-v1-table/dist/quill1-table.js",
"quill-emoji.js": "./static/lib/emoji.js",
"composer.js": "../nodebb-plugin-composer-default/static/lib/composer.js",
"composer/categoryList.js": "../nodebb-plugin-composer-default/static/lib/composer/categoryList.js",
Expand All @@ -46,4 +47,4 @@
"./node_modules/screenfull/dist/screenfull.js"
],
"templates": "static/templates"
}
}
Binary file added screenshots/table in editor.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/table in post.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 28 additions & 4 deletions static/lib/quill-nbb.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,20 +241,24 @@ $(window).on('action:chat.loaded', (evt, containerEl) => {

window.quill.init = function (targetEl, data, callback) {
require([
'quill', 'quill-magic-url', 'quill-emoji', 'quill-markdown-shortcuts',
'quill', 'quill-magic-url', 'quill-emoji', 'quill-markdown-shortcuts', 'quill-table',
'composer/autocomplete', 'composer/drafts',
], (Quill, MagicUrl, Emoji, MarkdownShortcuts, autocomplete, drafts) => {
], (Quill, MagicUrl, Emoji, MarkdownShortcuts, TableModule, autocomplete, drafts) => {
const textDirection = $('html').attr('data-dir');
const textareaEl = targetEl.siblings('textarea');

window.quill.configureToolbar(targetEl, data).then(({ toolbar }) => {
window.quill.configureToolbar(targetEl, data, TableModule).then(({ toolbar }) => {
// Quill...
Quill.register('modules/magicUrl', MagicUrl.default);
Quill.register('modules/markdownShortcuts', MarkdownShortcuts);
Quill.register('modules/table', TableModule);
const quill = new Quill(targetEl.get(0), {
theme: data.theme || 'snow',
modules: {
toolbar,
table: {
cellSelectionOnClick: false,
},
magicUrl: {
normalizeUrlOptions: {
sortQueryParameters: false,
Expand Down Expand Up @@ -383,7 +387,7 @@ window.quill.init = function (targetEl, data, callback) {
return window.quill;
};

window.quill.configureToolbar = async (targetEl, data) => {
window.quill.configureToolbar = async (targetEl, data, TableModule) => {
const textareaEl = targetEl.siblings('textarea');
const [formatting, hooks] = await new Promise((resolve) => {
require(['composer/formatting', 'hooks'], (...libs) => resolve(libs));
Expand All @@ -395,6 +399,26 @@ window.quill.configureToolbar = async (targetEl, data) => {
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['link', 'blockquote', 'code-block'],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ table: TableModule.tableOptions() }, {
table: [
'insert',
'remove-table',
'split-cell',
'merge-selection',
'append-row-above',
'append-row-below',
'append-col-before',
'append-col-after',
'remove-col',
'remove-row',
'remove-cell',
'remove-selection',
'hide-border',
'show-border',
'undo',
'redo',
],
}],
[{ script: 'sub' }, { script: 'super' }], // superscript/subscript
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
[{ align: [] }],
Expand Down
96 changes: 96 additions & 0 deletions static/scss/post.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,100 @@
.ql-font-monospace {
font-family: Monaco, "Courier New", monospace;
}

/* 表格视觉变量:默认浅色;仅当祖先带 .dark 或 .dark 时切换深色描边(不与系统 prefers-color-scheme 联动) */
table {
/* 原 0.14 在白底上极易被看成「无边框」;0.18 仍为浅边,可读性更好;可用变量改回 0.14 */
--ql-table-border: rgba(23, 26, 29, 0.18);
--ql-table-bg: transparent;
--ql-table-cell-bg: transparent;
--ql-table-selected-bg: rgba(59, 130, 246, 0.14);
--ql-table-font-size: 16px;
--ql-table-line-height: 1.7;
--ql-table-padding: 8px;
width: 100%;
border-collapse: collapse;
table-layout: fixed;
/* 勿与 border-radius 同用:在 Chromium/WebKit 下常导致 collapse 表格的格线被裁掉或完全不画 */
overflow: visible;
white-space: nowrap;
font-size: var(--ql-table-font-size);
line-height: var(--ql-table-line-height);
background-color: var(--ql-table-bg);
border-radius: 8px;
/* 外轮廓:collapse 下仅 td 边线在部分浏览器不可靠,表格外框保证浅色模式下可见 */
border: 1px solid var(--ql-table-border);
}

/* @media (prefers-color-scheme: dark) {
table {
--ql-table-border: rgba(255, 255, 255, 0.12);
--ql-table-selected-bg: rgba(96, 165, 250, 0.18);
}
} */

.dark table,
.dark table {
--ql-table-border: rgba(255, 255, 255, 0.12);
--ql-table-selected-bg: rgba(96, 165, 250, 0.18);
}

table td {
border: 1px solid var(--ql-table-border);
padding: var(--ql-table-padding);
min-height: calc(var(--ql-table-padding) * 2 + var(--ql-table-line-height) * 1em);
vertical-align: top;
white-space: pre-wrap; /* https://github.com/quilljs/quill/issues/1760 */
font-size: inherit;
line-height: inherit;
background-color: var(--ql-table-cell-bg);
}

.ql-editor__table--hideBorder td {
border: none !important;
}

.ql-editor__table--hideBorder {
border: none !important;
}

table td[rowspan="2"] {
min-height: calc(var(--ql-table-padding) * 2 + 2 * var(--ql-table-line-height) * 1em);
}

table td[rowspan="3"] {
min-height: calc(var(--ql-table-padding) * 2 + 3 * var(--ql-table-line-height) * 1em);
}

table td[rowspan="4"] {
min-height: calc(var(--ql-table-padding) * 2 + 4 * var(--ql-table-line-height) * 1em);
}

table td[rowspan="5"] {
min-height: calc(var(--ql-table-padding) * 2 + 5 * var(--ql-table-line-height) * 1em);
}

table td[rowspan="6"] {
min-height: calc(var(--ql-table-padding) * 2 + 6 * var(--ql-table-line-height) * 1em);
}

table td[rowspan="7"] {
min-height: calc(var(--ql-table-padding) * 2 + 7 * var(--ql-table-line-height) * 1em);
}

table td[rowspan="8"] {
min-height: calc(var(--ql-table-padding) * 2 + 8 * var(--ql-table-line-height) * 1em);
}

table td[rowspan="9"] {
min-height: calc(var(--ql-table-padding) * 2 + 9 * var(--ql-table-line-height) * 1em);
}

table td.ql-cell-selected {
background-color: var(--ql-table-selected-bg);
}

table td[merge_id] {
display: none;
}
}