diff --git a/assets/base.styl b/assets/base.styl index 8c15078fdb..66f8044d14 100644 --- a/assets/base.styl +++ b/assets/base.styl @@ -113,8 +113,6 @@ colorItemsPerRow = 7 font-size: 0.83em h6 font-size: 0.67em - pre - white-space: pre-wrap a text-decoration: underline blockquote @@ -128,6 +126,11 @@ colorItemsPerRow = 7 background-color: #f0f0f0 code, pre border-radius: 3px + pre + white-space: pre-wrap + margin-bottom: 5px + margin-top: 5px + padding: 5px 10px code font-size: 85% padding-bottom: 2px @@ -135,14 +138,6 @@ colorItemsPerRow = 7 &:before, &:after content: "\00a0" letter-spacing: -2px - *:not(pre) + pre, pre:first-of-type - margin-top: 5px - padding-top: 5px - pre - margin-bottom: 5px - padding: 0px 10px 5px - pre + pre - margin-top: -10px img max-width: 100% diff --git a/core/editor.js b/core/editor.js index f2bb52a90b..f7a60cf5be 100644 --- a/core/editor.js +++ b/core/editor.js @@ -2,6 +2,7 @@ import Delta from 'rich-text/lib/delta'; import DeltaOp from 'rich-text/lib/op'; import Emitter from './emitter'; import Parchment from 'parchment'; +import CodeBlock from '../formats/code'; import Block, { bubbleFormats } from '../blots/block'; import clone from 'clone'; import equal from 'deep-equal'; @@ -77,8 +78,15 @@ class Editor { formatLine(index, length, formats = {}, source = Emitter.sources.API) { this.scroll.update(); Object.keys(formats).forEach((format) => { - this.scroll.lines(index, Math.max(length, 1)).forEach(function(line) { - line.format(format, formats[format]); + let lines = this.scroll.lines(index, Math.max(length, 1)); + lines.forEach((line, i) => { + if (!(line instanceof CodeBlock)) { + line.format(format, formats[format]); + } else { + let codeIndex = index - line.offset(this.scroll); + let codeLength = line.newlineIndex(codeIndex) - index + 1; + line.formatAt(codeIndex, codeLength, format, formats[format]); + } }); }); this.scroll.optimize(); diff --git a/formats/code.js b/formats/code.js index efd6edf954..80feb83ed5 100644 --- a/formats/code.js +++ b/formats/code.js @@ -2,6 +2,7 @@ import Delta from 'rich-text/lib/delta'; import Parchment from 'parchment'; import Block from '../blots/block'; import Inline from '../blots/inline'; +import TextBlot from '../blots/text'; class Code extends Inline {} @@ -21,24 +22,83 @@ class CodeBlock extends Block { } delta() { - let text = this.descendants(Parchment.Leaf).map(function(leaf) { - return leaf instanceof Parchment.Text ? leaf.value() : ''; - }).join(''); - return new Delta().insert(text).insert('\n', this.formats()); + let text = this.domNode.textContent; + if (text.endsWith('\n')) { // Should always be true + text = text.slice(0, -1); + } + return text.split('\n').reduce((delta, frag) => { + return delta.insert(frag).insert('\n', this.formats()); + }, new Delta()); + } + + format(name, value) { + if (name === this.statics.blotName && value) return; + let [text, offset] = this.descendant(TextBlot, this.length() - 1); + if (text != null) { + text.deleteAt(text.length() - 1, 1); + } + super.format(name, value); } formatAt(index, length, name, value) { - if (Parchment.query(name, Parchment.Scope.BLOCK) || name === this.statics.blotName) { - super.formatAt(index, length, name, value); + if (length === 0) return; + if (Parchment.query(name, Parchment.Scope.BLOCK) == null || + (name === this.statics.blotName && value === this.statics.formats(this.domNode))) { + return; + } + let nextNewline = this.newlineIndex(index); + if (nextNewline < 0 || nextNewline >= index + length) return; + let prevNewline = this.newlineIndex(index, true) + 1; + let isolateLength = nextNewline - prevNewline + 1; + let blot = this.isolate(prevNewline, isolateLength); + let next = blot.next; + blot.format(name, value); + if (next instanceof CodeBlock) { + next.formatAt(0, index - prevNewline + length - isolateLength, name, value); + } + } + + insertAt(index, value, def) { + if (def != null) return; + let [text, offset] = this.descendant(TextBlot, index); + text.insertAt(offset, value); + } + + length() { + return this.domNode.textContent.length; + } + + newlineIndex(searchIndex, reverse = false) { + if (!reverse) { + let offset = this.domNode.textContent.slice(searchIndex).indexOf('\n'); + return offset > -1 ? searchIndex + offset : -1; + } else { + return this.domNode.textContent.slice(0, searchIndex).lastIndexOf('\n'); + } + } + + optimize() { + if (!this.domNode.textContent.endsWith('\n')) { + this.appendChild(Parchment.create('text', '\n')); + } + super.optimize(); + let next = this.next; + if (next != null && next.prev === this && + next.statics.blotName === this.statics.blotName && + this.statics.formats(this.domNode) === next.statics.formats(next.domNode)) { + next.optimize(); + next.moveChildren(this); + next.remove(); } } replace(target) { super.replace(target); - this.descendants(function(blot) { - return !(blot instanceof Parchment.Text); - }).forEach(function(blot) { - if (blot instanceof Parchment.Embed) { + [].slice.call(this.domNode.querySelectorAll('*')).forEach(function(node) { + let blot = Parchment.find(node); + if (blot == null) { + node.parentNode.removeChild(node); + } else if (blot instanceof Parchment.Embed) { blot.remove(); } else { blot.unwrap(); diff --git a/test/unit/formats/code.js b/test/unit/formats/code.js index b24ffe9eb5..8297b42992 100644 --- a/test/unit/formats/code.js +++ b/test/unit/formats/code.js @@ -9,6 +9,95 @@ describe('Code', function() { Parchment.register(CodeBlock); }); + it('newline', function() { + let editor = this.initialize(Editor, ` +

+      


+
\n
+


+
\n\n
+


+ `); + expect(editor.scroll.domNode).toEqualHTML(` +
\n
+


+
\n
+


+
\n\n
+


+ `); + }); + + it('default child', function() { + let editor = this.initialize(Editor, '


'); + editor.formatLine(0, 1, { 'code-block': true }); + expect(editor.scroll.domNode.innerHTML).toEqual('
\n
'); + }); + + it('merge', function() { + let editor = this.initialize(Editor, ` +
0
+
0
+


+
0
+
1\n
+


+
0
+
2\n\n
+


+
1\n
+
0
+


+
1\n
+
1\n
+


+
1\n
+
2\n\n
+


+
2\n\n
+
0
+


+
2\n\n
+
1\n
+


+
2\n\n
+
2\n\n
+ `); + editor.scroll.optimize(); + expect(editor.scroll.domNode).toEqualHTML(` +
0\n0\n
+


+
0\n1\n
+


+
0\n2\n\n
+


+
1\n0\n
+


+
1\n1\n
+


+
1\n2\n\n
+


+
2\n\n0\n
+


+
2\n\n1\n
+


+
2\n\n2\n\n
+ `); + }); + + it('merge multiple', function() { + let editor = this.initialize(Editor, ` +
0
+
1
+
2
+
3
+ `); + editor.scroll.optimize(); + expect(editor.scroll.domNode).toEqualHTML(` +
0\n1\n2\n3\n
+ `); + }); + it('add', function() { let editor = this.initialize(Editor, '

0123

5678

'); editor.formatLine(2, 5, { 'code-block': true }); @@ -16,7 +105,7 @@ describe('Code', function() { .insert('0123').insert('\n', { 'code-block': true }) .insert('5678').insert('\n', { 'code-block': true }) ); - expect(editor.scroll.domNode.innerHTML).toEqual('
0123
5678
'); + expect(editor.scroll.domNode.innerHTML).toEqual('
0123\n5678\n
'); }); it('remove', function() { @@ -33,6 +122,60 @@ describe('Code', function() { expect(editor.scroll.domNode).toEqualHTML('

0123

'); }); + it('replace multiple', function() { + let editor = this.initialize(Editor, { html: '
01\n23\n
' }); + editor.formatText(0, 6, { 'header': 1 }); + expect(editor.getDelta()).toEqual(new Delta() + .insert('01').insert('\n', { header: 1 }) + .insert('23').insert('\n', { header: 1 }) + ); + expect(editor.scroll.domNode).toEqualHTML('

01

23

'); + }); + + it('format interior line', function() { + let editor = this.initialize(Editor, { html: '
01\n23\n45\n
' }); + editor.formatText(5, 1, { 'header': 1 }); + expect(editor.getDelta()).toEqual(new Delta() + .insert('01').insert('\n', { 'code-block': true }) + .insert('23').insert('\n', { 'header': 1 }) + .insert('45').insert('\n', { 'code-block': true }) + ); + expect(editor.scroll.domNode.innerHTML).toEqual('
01\n

23

45\n
'); + }); + + it('format imprecise bounds', function() { + let editor = this.initialize(Editor, { html: '
01\n23\n45\n
' }); + editor.formatText(1, 6, { 'header': 1 }); + expect(editor.getDelta()).toEqual(new Delta() + .insert('01').insert('\n', { 'header': 1 }) + .insert('23').insert('\n', { 'header': 1 }) + .insert('45').insert('\n', { 'code-block': true }) + ); + expect(editor.scroll.domNode.innerHTML).toEqual('

01

23

45\n
'); + }); + + it('format without newline', function() { + let editor = this.initialize(Editor, { html: '
01\n23\n45\n
' }); + editor.formatText(3, 1, { 'header': 1 }); + expect(editor.getDelta()).toEqual(new Delta() + .insert('01').insert('\n', { 'code-block': true }) + .insert('23').insert('\n', { 'code-block': true }) + .insert('45').insert('\n', { 'code-block': true }) + ); + expect(editor.scroll.domNode.innerHTML).toEqual('
01\n23\n45\n
'); + }); + + it('format line', function() { + let editor = this.initialize(Editor, { html: '
01\n23\n45\n
' }); + editor.formatLine(3, 1, { 'header': 1 }); + expect(editor.getDelta()).toEqual(new Delta() + .insert('01').insert('\n', { 'code-block': true }) + .insert('23').insert('\n', { 'header': 1 }) + .insert('45').insert('\n', { 'code-block': true }) + ); + expect(editor.scroll.domNode.innerHTML).toEqual('
01\n

23

45\n
'); + }); + it('ignore formatAt', function() { let editor = this.initialize(Editor, '
0123
'); editor.formatText(1, 1, { bold: true });