Browse Source

Merge pull request #2270 from ellisonbg/vineetgorhe-markdown_boundaries

Adds markdown code block detection
Brian E. Granger 8 years ago
parent
commit
aa82567798

+ 2 - 0
packages/coreutils/src/index.ts

@@ -16,3 +16,5 @@ export * from './url';
 export * from './uuid';
 export * from './vector';
 export * from './modeldb';
+export * from './markdowncodeblocks';
+

+ 117 - 0
packages/coreutils/src/markdowncodeblocks.ts

@@ -0,0 +1,117 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+/**
+ * The namespace for code block functions which help
+ * in extract code from markdown text
+ */
+export
+namespace MarkdownCodeBlocks {
+  export
+  const markdownMarkers: string[] = ["```", "~~~~", "`"]
+  const markdownExtensions: string[] = [
+    '.markdown',
+    '.mdown',
+    '.mkdn',
+    '.md',
+    '.mkd',
+    '.mdwn',
+    '.mdtxt',
+    '.mdtext',
+    '.text',
+    '.txt',
+    '.Rmd'
+  ];
+
+  export
+  class MarkdownCodeBlock {
+    startLine: number;
+    endLine: number;
+    code: string;
+    constructor(startLine: number) {
+      this.startLine = startLine;
+      this.code = "";
+      this.endLine = -1;
+    }
+  }
+
+  /**
+  * Check whether the given file extension is a markdown extension
+  * @param extension - A file extension
+  *
+  * @returns true/false depending on whether this is a supported markdown extension
+  */
+  export
+  function isMarkdown(extension: string): boolean {
+    return markdownExtensions.indexOf(extension) > -1
+  }
+
+  /**
+  * Construct all code snippets from current text
+  * (this could be potentially optimized if we can cache and detect differences)
+  * @param text - A string to parse codeblocks from
+  *
+  * @returns An array of MarkdownCodeBlocks.
+  */
+  export
+  function findMarkdownCodeBlocks(text: string): MarkdownCodeBlock[] {
+    if (!text || text == '') {
+      return [];
+    }
+
+    const lines = text.split("\n");
+    const codeSnippets: MarkdownCodeBlock[] = [];
+    var currentCode = null;
+    for (var lineIndex = 0; lineIndex < lines.length; lineIndex++) {
+      const line = lines[lineIndex];
+      const marker = findNextMarker(line);
+      const lineContainsMarker = marker != '';
+      const constructingSnippet = currentCode != null;
+      //skip this line if it is not part of any code snippet and doesn't contain a marker
+      if (!lineContainsMarker && !constructingSnippet) {
+        continue;
+      }
+
+      //check if we are already constructing a code snippet
+      if (!constructingSnippet) {
+        //start constructing
+        currentCode = new MarkdownCodeBlock(lineIndex);
+
+        //check whether this is a single line code snippet
+        const firstIndex = line.indexOf(marker);
+        const lastIndex = line.lastIndexOf(marker);
+        const isSingleLine = firstIndex != lastIndex
+        if (isSingleLine) {
+          currentCode.code = line.substring(firstIndex + marker.length, lastIndex);
+          currentCode.endLine = lineIndex;
+          codeSnippets.push(currentCode);
+          currentCode = null;
+        } else {
+          currentCode.code = line.substring(firstIndex + marker.length);
+        }
+      } else {
+        //already constructing
+        if (lineContainsMarker) {
+          currentCode.code += "\n" + line.substring(0, line.indexOf(marker));
+          currentCode.endLine = lineIndex;
+          codeSnippets.push(currentCode);
+          currentCode = null;
+        } else {
+          currentCode.code += "\n" + line;
+        }
+      }
+    }
+    return codeSnippets;
+  }
+
+
+  function findNextMarker(text: string) {
+    for (let marker of markdownMarkers) {
+      const index = text.indexOf(marker);
+      if (index > -1) {
+        return marker;
+      }
+    }
+    return '';
+  }
+}

+ 2 - 1
packages/fileeditor-extension/package.json

@@ -17,7 +17,8 @@
     "@jupyterlab/codeeditor": "^0.5.0",
     "@jupyterlab/docregistry": "^0.5.0",
     "@jupyterlab/fileeditor": "^0.5.0",
-    "@jupyterlab/launcher": "^0.5.0"
+    "@jupyterlab/launcher": "^0.5.0",
+    "@jupyterlab/coreutils": "^0.5.0"
   },
   "devDependencies": {
     "rimraf": "^2.5.2",

+ 42 - 8
packages/fileeditor-extension/src/index.ts

@@ -29,6 +29,10 @@ import {
   ILauncher
 } from '@jupyterlab/launcher';
 
+import {
+   MarkdownCodeBlocks
+} from '@jupyterlab/coreutils'
+
 
 /**
  * The class name for the text editor icon from the default theme.
@@ -174,6 +178,21 @@ function activate(app: JupyterLab, registry: IDocumentRegistry, restorer: ILayou
     return tracker.currentWidget !== null;
   }
 
+  /** To detect if there is no current selection */
+  function hasSelection(): boolean {
+    let widget = tracker.currentWidget;
+    if (!widget) {
+      return false;
+    }
+    const editor = widget.editor;
+    const selection = editor.getSelection();
+    if (selection.start.column == selection.end.column &&
+      selection.start.line == selection.end.line) {
+      return false;
+    }
+    return true;
+  }
+
   commands.addCommand(CommandIDs.lineNumbers, {
     execute: toggleLineNums,
     isEnabled: hasWidget,
@@ -211,18 +230,33 @@ function activate(app: JupyterLab, registry: IDocumentRegistry, restorer: ILayou
       if (!widget) {
         return;
       }
-      // Get the selected code from the editor.
+
+      var code = ""
       const editor = widget.editor;
-      const selection = editor.getSelection();
-      const start = editor.getOffsetAt(selection.start);
-      const end = editor.getOffsetAt(selection.end);
-      let targetText = editor.model.value.text.substring(start, end);
-      if (start == end) {
-        targetText = editor.getLine(selection.start.line); 
+      const extension = widget.context.path.substring(widget.context.path.lastIndexOf("."));
+      if (!hasSelection() && MarkdownCodeBlocks.isMarkdown(extension)) {
+        var codeBlocks = MarkdownCodeBlocks.findMarkdownCodeBlocks(editor.model.value.text);
+        for (let codeBlock of codeBlocks) {
+          if (codeBlock.startLine <= editor.getSelection().start.line &&
+            editor.getSelection().start.line <= codeBlock.endLine) {
+            code = codeBlock.code;
+            break;
+          }
+        }
+      } else {
+        // Get the selected code from the editor.
+        const selection = editor.getSelection();
+        const start = editor.getOffsetAt(selection.start);
+        const end = editor.getOffsetAt(selection.end);
+        code = editor.model.value.text.substring(start, end);
+        if (start == end) {
+          code = editor.getLine(selection.start.line); 
+        }
       }
+
       const options: JSONObject = {
         path: widget.context.path,
-        code: targetText,
+        code: code,
         activate: false
       };
       // Advance cursor to the next line.

+ 103 - 0
test/src/coreutils/markdowncodeblocks.spec.ts

@@ -0,0 +1,103 @@
+// Copyright (c) Jupyter Development Team.
+// Distributed under the terms of the Modified BSD License.
+
+import {
+  expect
+} from 'chai';
+
+import {
+  MarkdownCodeBlocks
+} from '@jupyterlab/coreutils';
+
+
+describe('@jupyterlab/coreutils', () => {
+
+  describe('MarkdownCodeBlocks', () => {
+
+    describe('.isMarkdown()', () => {
+      it('should return true for a valid markdown extension', () => {
+        let isMarkdown = MarkdownCodeBlocks.isMarkdown(".md");
+        expect(isMarkdown).true
+      });
+
+    });
+
+    function performTest(text: string, startLine: number, endLine: number) {
+      for (let marker of MarkdownCodeBlocks.markdownMarkers) {
+        let markdown1 = marker + text + marker;
+        let cb = MarkdownCodeBlocks.findMarkdownCodeBlocks(markdown1)[0];
+        expect(cb.code).to.equal(text);
+        expect(cb.startLine).to.equal(startLine);
+        expect(cb.endLine).to.equal(endLine);
+      }
+    }
+
+    describe('.findMarkdownCodeBlocks()', () => {
+      it('should return the codeblock for all delimiters', () => {
+        performTest("print(\"foobar\");\nprint(\"blahblah\")", 0, 1);
+      });
+
+      it('should return all codeblocks for multiline text', () => {
+        let text = `print("foo");
+        import os;
+        print("helloworld!")`;
+        performTest(text, 0, 2);
+      });
+
+      it('should return all codeblocks for text containing multiple delimiters', () => {
+        let text = `
+          pop goes the weasel!
+          \`print("amazing!");\`
+          \`
+          print("amazing!");
+          \`
+          \`print("amazing!");
+          \`
+          \`
+          print("amazing!");\`
+          
+          \`\`\`print("with triple quotes");\`\`\`
+          \`\`\`print("with triple quotes");
+          print("and multiline");
+          \`\`\`
+          \`\`\`
+          print("with triple quotes");
+          \`\`\`
+          \`\`\`
+          print("with triple quotes");\`\`\`
+
+          wheels on the bus go round and round!
+
+          ~~~~print("how about this?");~~~~
+          ~~~~
+          print("how about this?");
+          ~~~~
+          ~~~~
+          print("how about this?");~~~~
+          ~~~~print("how about this?");
+          ~~~~
+        `;
+
+        let codeblocks = MarkdownCodeBlocks.findMarkdownCodeBlocks(text);
+        expect(codeblocks.length).to.equal(12);
+        expect(codeblocks[0].code, 'cb0').to.equal('print("amazing!");')
+        expect(codeblocks[1].code, 'cb1').to.equal('\n          print("amazing!");\n          ')
+        expect(codeblocks[2].code, 'cb2').to.equal('print("amazing!");\n          ')
+        expect(codeblocks[3].code, 'cb3').to.equal('\n          print("amazing!");')
+
+        expect(codeblocks[4].code, 'cb4').to.equal('print("with triple quotes");')
+        expect(codeblocks[5].code, 'cb5').to.equal('print("with triple quotes");\n          print("and multiline");\n          ');
+        expect(codeblocks[6].code, 'cb6').to.equal('\n          print("with triple quotes");\n          ')
+        expect(codeblocks[7].code, 'cb7').to.equal('\n          print("with triple quotes");')
+
+        expect(codeblocks[8].code, 'cb8').to.equal('print("how about this?");')
+        expect(codeblocks[9].code, 'cb9').to.equal('\n          print("how about this?");\n          ')
+        expect(codeblocks[10].code, 'cb10').to.equal('\n          print("how about this?");')
+        expect(codeblocks[11].code, 'cb11').to.equal('print("how about this?");\n          ')
+
+      });
+    });
+
+  });
+
+});

+ 1 - 0
test/src/index.ts

@@ -50,6 +50,7 @@ import './coreutils/time.spec';
 import './coreutils/undoablevector.spec';
 import './coreutils/url.spec';
 import './coreutils/uuid.spec';
+import './coreutils/markdowncodeblocks.spec';
 
 import './csvviewer/table.spec';
 import './csvviewer/toolbar.spec';