From 7cc1683be9ec4cfd7613988800e22f426a7b12bb Mon Sep 17 00:00:00 2001 From: Jason Chen Date: Wed, 6 Jul 2016 23:50:14 -0700 Subject: [PATCH] Reimplement code block to use one

fixes #723
---
 assets/base.styl          |  15 ++--
 core/editor.js            |  12 +++-
 formats/code.js           |  80 ++++++++++++++++++---
 test/unit/formats/code.js | 145 +++++++++++++++++++++++++++++++++++++-
 4 files changed, 229 insertions(+), 23 deletions(-)

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 });