From 6b54279bb7617b4ea604193f47532b0b302b0ee3 Mon Sep 17 00:00:00 2001 From: Alec Merdler Date: Wed, 10 May 2017 14:18:37 -0700 Subject: [PATCH] Markdown Overhaul (#2624) Rebuilt Markdown editor/views into new components --- package.json | 3 + static/css/directives/ui/markdown-editor.css | 31 - .../css/directives/ui/quay-service-status.css | 4 +- static/directives/image-info-sidebar.html | 2 +- static/directives/markdown-editor.html | 11 - static/directives/markdown-input.html | 31 - static/directives/markdown-view.html | 1 - static/directives/quay-message-bar.html | 2 +- static/directives/repo-list-grid.html | 5 +- .../directives/repo-view/repo-panel-info.html | 10 +- static/directives/service-keys-manager.html | 2 +- static/js/constants/platform.constant.ts | 19 + .../directives/repo-view/repo-panel-info.js | 1 - .../app-public-view.component.html | 10 +- static/js/directives/ui/markdown-editor.js | 32 - static/js/directives/ui/markdown-input.js | 49 - static/js/directives/ui/markdown-view.js | 26 - .../ui/markdown/markdown-editor.component.css | 24 + .../markdown/markdown-editor.component.html | 43 + .../markdown-editor.component.spec.ts | 147 ++ .../ui/markdown/markdown-editor.component.ts | 133 ++ .../ui/markdown/markdown-input.component.css | 14 + .../ui/markdown/markdown-input.component.html | 29 + .../markdown/markdown-input.component.spec.ts | 34 + .../ui/markdown/markdown-input.component.ts | 32 + .../markdown/markdown-toolbar.component.css | 8 + .../markdown/markdown-toolbar.component.html | 61 + .../markdown-toolbar.component.spec.ts | 11 + .../ui/markdown/markdown-toolbar.component.ts | 17 + .../ui/markdown/markdown-view.component.css | 11 + .../ui/markdown/markdown-view.component.html | 2 + .../markdown/markdown-view.component.spec.ts | 81 + .../ui/markdown/markdown-view.component.ts | 51 + .../ui/search-box/search-box.component.html | 7 +- static/js/pages/new-repo.js | 4 + static/js/quay.module.ts | 13 +- static/js/types/common.types.ts | 20 +- static/lib/pagedown/LICENSE.txt | 32 - static/lib/pagedown/Markdown.Converter.js | 1346 ----------- .../lib/pagedown/Markdown.Editor.Icons.fw.png | Bin 51360 -> 0 bytes static/lib/pagedown/Markdown.Editor.Icons.png | Bin 52640 -> 0 bytes static/lib/pagedown/Markdown.Editor.js | 2114 ----------------- static/lib/pagedown/Markdown.Editor.less | 80 - static/lib/pagedown/Markdown.Sanitizer.js | 111 - static/partials/new-repo.html | 8 +- static/partials/search.html | 3 +- static/partials/super-user.html | 3 +- static/partials/team-view.html | 10 +- webpack.config.js | 5 +- yarn.lock | 22 +- 50 files changed, 819 insertions(+), 3896 deletions(-) delete mode 100644 static/css/directives/ui/markdown-editor.css delete mode 100644 static/directives/markdown-editor.html delete mode 100644 static/directives/markdown-input.html delete mode 100644 static/directives/markdown-view.html create mode 100644 static/js/constants/platform.constant.ts delete mode 100644 static/js/directives/ui/markdown-editor.js delete mode 100644 static/js/directives/ui/markdown-input.js delete mode 100644 static/js/directives/ui/markdown-view.js create mode 100644 static/js/directives/ui/markdown/markdown-editor.component.css create mode 100644 static/js/directives/ui/markdown/markdown-editor.component.html create mode 100644 static/js/directives/ui/markdown/markdown-editor.component.spec.ts create mode 100644 static/js/directives/ui/markdown/markdown-editor.component.ts create mode 100644 static/js/directives/ui/markdown/markdown-input.component.css create mode 100644 static/js/directives/ui/markdown/markdown-input.component.html create mode 100644 static/js/directives/ui/markdown/markdown-input.component.spec.ts create mode 100644 static/js/directives/ui/markdown/markdown-input.component.ts create mode 100644 static/js/directives/ui/markdown/markdown-toolbar.component.css create mode 100644 static/js/directives/ui/markdown/markdown-toolbar.component.html create mode 100644 static/js/directives/ui/markdown/markdown-toolbar.component.spec.ts create mode 100644 static/js/directives/ui/markdown/markdown-toolbar.component.ts create mode 100644 static/js/directives/ui/markdown/markdown-view.component.css create mode 100644 static/js/directives/ui/markdown/markdown-view.component.html create mode 100644 static/js/directives/ui/markdown/markdown-view.component.spec.ts create mode 100644 static/js/directives/ui/markdown/markdown-view.component.ts delete mode 100755 static/lib/pagedown/LICENSE.txt delete mode 100755 static/lib/pagedown/Markdown.Converter.js delete mode 100755 static/lib/pagedown/Markdown.Editor.Icons.fw.png delete mode 100755 static/lib/pagedown/Markdown.Editor.Icons.png delete mode 100755 static/lib/pagedown/Markdown.Editor.js delete mode 100755 static/lib/pagedown/Markdown.Editor.less delete mode 100755 static/lib/pagedown/Markdown.Sanitizer.js diff --git a/package.json b/package.json index 12d938a2d..b84f67b24 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "react-dom": "^15.3.2", "restangular": "^1.2.0", "rxjs": "^5.0.1", + "showdown": "^1.6.4", + "showdown-highlightjs-extension": "^0.1.2", "underscore": "^1.5.2" }, "devDependencies": { @@ -47,6 +49,7 @@ "@types/jquery": "^2.0.40", "@types/react": "0.14.39", "@types/react-dom": "0.14.17", + "@types/showdown": "^1.4.32", "angular-mocks": "1.6.2", "angular-ts-decorators": "0.0.19", "css-loader": "0.25.0", diff --git a/static/css/directives/ui/markdown-editor.css b/static/css/directives/ui/markdown-editor.css deleted file mode 100644 index bf5602db7..000000000 --- a/static/css/directives/ui/markdown-editor.css +++ /dev/null @@ -1,31 +0,0 @@ -.markdown-editor-element .wmd-panel .btn { - background-color: #ddd; -} - -.markdown-editor-element .wmd-panel .btn:hover { - background-color: #eee; -} - -.markdown-editor-element .wmd-panel .btn:active { - background-color: #ccc; -} - -.markdown-editor-element .preview-btn { - float: right; -} - -.markdown-editor-element .preview-btn.active { - box-shadow: inset 0 3px 5px rgba(0,0,0,.125); -} - -.markdown-editor-element .preview-panel .markdown-view { - border: 1px solid #eee; - padding: 4px; - min-height: 150px; -} - -.markdown-editor-element .preview-top-bar { - height: 43px; - line-height: 43px; - color: #ddd; -} \ No newline at end of file diff --git a/static/css/directives/ui/quay-service-status.css b/static/css/directives/ui/quay-service-status.css index 367a7dede..13e63d21b 100644 --- a/static/css/directives/ui/quay-service-status.css +++ b/static/css/directives/ui/quay-service-status.css @@ -9,7 +9,9 @@ } .quay-service-status-description { - vertical-align: middle; + display: flex; + justify-content: center; + align-items: center; } .quay-service-status-indicator.none { diff --git a/static/directives/image-info-sidebar.html b/static/directives/image-info-sidebar.html index 0fd2984aa..fe32591d7 100644 --- a/static/directives/image-info-sidebar.html +++ b/static/directives/image-info-sidebar.html @@ -2,7 +2,7 @@
- +
diff --git a/static/directives/markdown-editor.html b/static/directives/markdown-editor.html deleted file mode 100644 index 4f838dffe..000000000 --- a/static/directives/markdown-editor.html +++ /dev/null @@ -1,11 +0,0 @@ -
- Preview -
-
- -
-
-
Viewing preview
-
-
-
\ No newline at end of file diff --git a/static/directives/markdown-input.html b/static/directives/markdown-input.html deleted file mode 100644 index d3fd5cbec..000000000 --- a/static/directives/markdown-input.html +++ /dev/null @@ -1,31 +0,0 @@ -
-

- - Click to set {{ fieldTitle }} - -

- - - -
diff --git a/static/directives/markdown-view.html b/static/directives/markdown-view.html deleted file mode 100644 index c47184387..000000000 --- a/static/directives/markdown-view.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/static/directives/quay-message-bar.html b/static/directives/quay-message-bar.html index cf6b84b03..cc6de4a1c 100644 --- a/static/directives/quay-message-bar.html +++ b/static/directives/quay-message-bar.html @@ -3,7 +3,7 @@
- + {{ message.content }} diff --git a/static/directives/repo-list-grid.html b/static/directives/repo-list-grid.html index 29aa59f97..c2f9b9675 100644 --- a/static/directives/repo-list-grid.html +++ b/static/directives/repo-list-grid.html @@ -41,7 +41,10 @@ star-toggled="starToggled({'repository': repository})">
-
+ diff --git a/static/directives/repo-view/repo-panel-info.html b/static/directives/repo-view/repo-panel-info.html index a0f77fa71..341b3855d 100644 --- a/static/directives/repo-view/repo-panel-info.html +++ b/static/directives/repo-view/repo-panel-info.html @@ -57,11 +57,11 @@

Description

-
+
+
diff --git a/static/directives/service-keys-manager.html b/static/directives/service-keys-manager.html index 964523dab..4f043861a 100644 --- a/static/directives/service-keys-manager.html +++ b/static/directives/service-keys-manager.html @@ -167,7 +167,7 @@
Approval notes
-
+
diff --git a/static/js/constants/platform.constant.ts b/static/js/constants/platform.constant.ts new file mode 100644 index 000000000..b309c9043 --- /dev/null +++ b/static/js/constants/platform.constant.ts @@ -0,0 +1,19 @@ +/** + * Type representing current browser platform. + * TODO: Add more browser platforms. + */ +export type BrowserPlatform = "firefox" + | "chrome"; + +/** + * Constant representing current browser platform. Used for determining available features. + * TODO Only rudimentary implementation, should prefer specific feature detection strategies instead. + */ +export const browserPlatform: BrowserPlatform = (() => { + if (navigator.userAgent.toLowerCase().indexOf('firefox') != -1) { + return 'firefox'; + } + else { + return 'chrome'; + } + })(); diff --git a/static/js/directives/repo-view/repo-panel-info.js b/static/js/directives/repo-view/repo-panel-info.js index eb4d9ed48..58cb1e038 100644 --- a/static/js/directives/repo-view/repo-panel-info.js +++ b/static/js/directives/repo-view/repo-panel-info.js @@ -50,4 +50,3 @@ angular.module('quay').directive('repoPanelInfo', function () { }; return directiveDefinitionObject; }); - diff --git a/static/js/directives/ui/app-public-view/app-public-view.component.html b/static/js/directives/ui/app-public-view/app-public-view.component.html index fd9506d59..1375b2d85 100644 --- a/static/js/directives/ui/app-public-view/app-public-view.component.html +++ b/static/js/directives/ui/app-public-view/app-public-view.component.html @@ -30,11 +30,11 @@ -
+
+
diff --git a/static/js/directives/ui/markdown-editor.js b/static/js/directives/ui/markdown-editor.js deleted file mode 100644 index e68b36a3a..000000000 --- a/static/js/directives/ui/markdown-editor.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * An element which display an inline editor for writing and previewing markdown text. - */ -angular.module('quay').directive('markdownEditor', function () { - var counter = 0; - - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/markdown-editor.html', - replace: false, - transclude: false, - restrict: 'C', - scope: { - 'content': '=content', - }, - controller: function($scope, $element, $timeout) { - $scope.id = (counter++); - $scope.previewing = false; - - $timeout(function() { - var converter = Markdown.getSanitizingConverter(); - var editor = new Markdown.Editor(converter, '-' + $scope.id); - editor.run(); - }); - - $scope.togglePreview = function() { - $scope.previewing = !$scope.previewing; - }; - } - }; - return directiveDefinitionObject; -}); diff --git a/static/js/directives/ui/markdown-input.js b/static/js/directives/ui/markdown-input.js deleted file mode 100644 index aad6b1a27..000000000 --- a/static/js/directives/ui/markdown-input.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * An element which allows for entry of markdown content and previewing its rendering. - */ -angular.module('quay').directive('markdownInput', function () { - var counter = 0; - - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/markdown-input.html', - replace: false, - transclude: false, - restrict: 'C', - scope: { - 'content': '=content', - 'canWrite': '=canWrite', - 'contentChanged': '=contentChanged', - 'fieldTitle': '=fieldTitle' - }, - controller: function($scope, $element) { - var elm = $element[0]; - - $scope.id = (counter++); - - $scope.editContent = function() { - if (!$scope.canWrite) { return; } - - if (!$scope.markdownDescriptionEditor) { - var converter = Markdown.getSanitizingConverter(); - var editor = new Markdown.Editor(converter, '-description-' + $scope.id); - editor.run(); - $scope.markdownDescriptionEditor = editor; - } - - $('#wmd-input-description-' + $scope.id)[0].value = $scope.content; - $(elm).find('.modal').modal({}); - }; - - $scope.saveContent = function() { - $scope.content = $('#wmd-input-description-' + $scope.id)[0].value; - $(elm).find('.modal').modal('hide'); - - if ($scope.contentChanged) { - $scope.contentChanged($scope.content); - } - }; - } - }; - return directiveDefinitionObject; -}); diff --git a/static/js/directives/ui/markdown-view.js b/static/js/directives/ui/markdown-view.js deleted file mode 100644 index fcd598501..000000000 --- a/static/js/directives/ui/markdown-view.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * An element which displays its content processed as markdown. - */ -angular.module('quay').directive('markdownView', function () { - var directiveDefinitionObject = { - priority: 0, - templateUrl: '/static/directives/markdown-view.html', - replace: false, - transclude: false, - restrict: 'C', - scope: { - 'content': '=content', - 'firstLineOnly': '=firstLineOnly', - 'placeholderNeeded': '=placeholderNeeded' - }, - controller: function($scope, $element, $sce, UtilService) { - $scope.getMarkedDown = function(content, firstLineOnly) { - if (firstLineOnly) { - return $sce.trustAsHtml(UtilService.getFirstMarkdownLineAsText(content, $scope.placeholderNeeded)); - } - return $sce.trustAsHtml(UtilService.getMarkedDown(content)); - }; - } - }; - return directiveDefinitionObject; -}); diff --git a/static/js/directives/ui/markdown/markdown-editor.component.css b/static/js/directives/ui/markdown/markdown-editor.component.css new file mode 100644 index 000000000..dcf129171 --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-editor.component.css @@ -0,0 +1,24 @@ +.markdown-editor-element textarea { + height: 300px; + resize: vertical; + width: 100%; +} + +.markdown-editor-actions { + display: flex; + justify-content: flex-end; + margin-top: 20px; +} + +.markdown-editor-buttons { + display: flex; + justify-content: space-between; +} + +.markdown-editor-buttons button { + margin: 0 10px; +} + +.markdown-editor-buttons button:last-child { + margin: 0; +} diff --git a/static/js/directives/ui/markdown/markdown-editor.component.html b/static/js/directives/ui/markdown/markdown-editor.component.html new file mode 100644 index 000000000..abb632938 --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-editor.component.html @@ -0,0 +1,43 @@ +
+ + + +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+
diff --git a/static/js/directives/ui/markdown/markdown-editor.component.spec.ts b/static/js/directives/ui/markdown/markdown-editor.component.spec.ts new file mode 100644 index 000000000..e3effd200 --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-editor.component.spec.ts @@ -0,0 +1,147 @@ +import { MarkdownEditorComponent, EditMode } from './markdown-editor.component'; +import { MarkdownSymbol } from '../../../types/common.types'; +import { Mock } from 'ts-mocks'; +import Spy = jasmine.Spy; + + +describe("MarkdownEditorComponent", () => { + var component: MarkdownEditorComponent; + var textarea: Mock; + var documentMock: Mock; + + beforeEach(() => { + textarea = new Mock(); + documentMock = new Mock(); + const $documentMock: any = [documentMock.Object]; + component = new MarkdownEditorComponent($documentMock, 'chrome'); + component.textarea = textarea.Object; + }); + + describe("changeEditMode", () => { + + it("sets component's edit mode to given mode", () => { + const editMode: EditMode = "preview"; + component.changeEditMode(editMode); + + expect(component.currentEditMode).toEqual(editMode); + }); + }); + + describe("insertSymbol", () => { + var event: {symbol: MarkdownSymbol}; + var markdownSymbols: {type: MarkdownSymbol, characters: string, shiftBy: number}[]; + var innerText: string; + + beforeEach(() => { + event = {symbol: 'heading1'}; + innerText = "Here is some text"; + markdownSymbols = [ + {type: 'heading1', characters: '# ', shiftBy: 2}, + {type: 'heading2', characters: '## ', shiftBy: 3}, + {type: 'heading3', characters: '### ', shiftBy: 4}, + {type: 'bold', characters: '****', shiftBy: 2}, + {type: 'italics', characters: '__', shiftBy: 1}, + {type: 'bulleted-list', characters: '- ', shiftBy: 2}, + {type: 'numbered-list', characters: '1. ', shiftBy: 3}, + {type: 'quote', characters: '> ', shiftBy: 2}, + {type: 'link', characters: '[](url)', shiftBy: 1}, + {type: 'code', characters: '``', shiftBy: 1}, + ]; + + textarea.setup(mock => mock.focus); + textarea.setup(mock => mock.substr).is((start, end) => ''); + textarea.setup(mock => mock.val).is((value?) => innerText); + textarea.setup(mock => mock.prop).is((prop) => { + switch (prop) { + case "selectionStart": + return 0; + case "selectionEnd": + return 0; + } + }); + documentMock.setup(mock => mock.execCommand).is((commandID, showUI, value) => false); + }); + + it("focuses on markdown textarea", () => { + component.insertSymbol(event); + + expect(textarea.Object.focus).toHaveBeenCalled(); + }); + + it("inserts correct characters for given symbol at cursor position", () => { + markdownSymbols.forEach((symbol) => { + event.symbol = symbol.type; + component.insertSymbol(event); + + expect((documentMock.Object.execCommand).calls.argsFor(0)[0]).toEqual('insertText'); + expect((documentMock.Object.execCommand).calls.argsFor(0)[1]).toBe(false); + expect((documentMock.Object.execCommand).calls.argsFor(0)[2]).toEqual(symbol.characters); + + (documentMock.Object.execCommand).calls.reset(); + }); + }); + + it("splices highlighted selection between inserted characters instead of deleting them", () => { + markdownSymbols.slice(0, 1).forEach((symbol) => { + textarea.setup(mock => mock.prop).is((prop) => { + switch (prop) { + case "selectionStart": + return 0; + case "selectionEnd": + return innerText.length; + } + }); + event.symbol = symbol.type; + component.insertSymbol(event); + + expect((documentMock.Object.execCommand).calls.argsFor(0)[0]).toEqual('insertText'); + expect((documentMock.Object.execCommand).calls.argsFor(0)[1]).toBe(false); + expect((documentMock.Object.execCommand).calls.argsFor(0)[2]).toEqual(`${symbol.characters.slice(0, symbol.shiftBy)}${innerText}${symbol.characters.slice(symbol.shiftBy, symbol.characters.length)}`); + + (documentMock.Object.execCommand).calls.reset(); + }); + }); + + it("moves cursor to correct position for given symbol", () => { + markdownSymbols.forEach((symbol) => { + event.symbol = symbol.type; + component.insertSymbol(event); + + expect((textarea.Object.prop).calls.argsFor(2)[0]).toEqual('selectionStart'); + expect((textarea.Object.prop).calls.argsFor(2)[1]).toEqual(symbol.shiftBy); + expect((textarea.Object.prop).calls.argsFor(3)[0]).toEqual('selectionEnd'); + expect((textarea.Object.prop).calls.argsFor(3)[1]).toEqual(symbol.shiftBy); + + (textarea.Object.prop).calls.reset(); + }); + }); + }); + + describe("saveChanges", () => { + + beforeEach(() => { + component.content = "# Some markdown content"; + }); + + it("emits output event with changed content", (done) => { + component.save.subscribe((event: {editedContent: string}) => { + expect(event.editedContent).toEqual(component.content); + done(); + }); + + component.saveChanges(); + }); + }); + + describe("discardChanges", () => { + + it("emits output event with no content", (done) => { + component.discard.subscribe((event: {}) => { + expect(event).toEqual({}); + done(); + }); + + component.discardChanges(); + }); + }); +}); diff --git a/static/js/directives/ui/markdown/markdown-editor.component.ts b/static/js/directives/ui/markdown/markdown-editor.component.ts new file mode 100644 index 000000000..97833ac17 --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-editor.component.ts @@ -0,0 +1,133 @@ +import { Component, Inject, Input, Output, EventEmitter, ViewChild } from 'ng-metadata/core'; +import { MarkdownSymbol } from '../../../types/common.types'; +import { BrowserPlatform } from '../../../constants/platform.constant'; +import './markdown-editor.component.css'; + + +/** + * An editing interface for Markdown content. + */ +@Component({ + selector: 'markdown-editor', + templateUrl: '/static/js/directives/ui/markdown/markdown-editor.component.html' +}) +export class MarkdownEditorComponent { + + @Input('<') public content: string; + @Output() public save: EventEmitter<{editedContent: string}> = new EventEmitter(); + @Output() public discard: EventEmitter = new EventEmitter(); + // Textarea is public for testability, should not be directly accessed + @ViewChild('#markdown-textarea') public textarea: ng.IAugmentedJQuery; + private editMode: EditMode = "write"; + + constructor(@Inject('$document') private $document: ng.IDocumentService, + @Inject('BrowserPlatform') private browserPlatform: BrowserPlatform) { + + } + + public changeEditMode(newMode: EditMode): void { + this.editMode = newMode; + } + + public insertSymbol(event: {symbol: MarkdownSymbol}): void { + this.textarea.focus(); + + const startPos: number = this.textarea.prop('selectionStart'); + const endPos: number = this.textarea.prop('selectionEnd'); + const innerText: string = this.textarea.val().slice(startPos, endPos); + var shiftBy: number = 0; + var characters: string = ''; + + switch (event.symbol) { + case 'heading1': + characters = '# '; + shiftBy = 2; + break; + case 'heading2': + characters = '## '; + shiftBy = 3; + break; + case 'heading3': + characters = '### '; + shiftBy = 4; + break; + case 'bold': + characters = '****'; + shiftBy = 2; + break; + case 'italics': + characters = '__'; + shiftBy = 1; + break; + case 'bulleted-list': + characters = '- '; + shiftBy = 2; + break; + case 'numbered-list': + characters = '1. '; + shiftBy = 3; + break; + case 'quote': + characters = '> '; + shiftBy = 2; + break; + case 'link': + characters = '[](url)'; + shiftBy = 1; + break; + case 'code': + characters = '``'; + shiftBy = 1; + break; + } + + const cursorPos: number = startPos + shiftBy; + + if (startPos != endPos) { + this.insertText(`${characters.slice(0, shiftBy)}${innerText}${characters.slice(shiftBy, characters.length)}`, + startPos, + endPos); + } + else { + this.insertText(characters, startPos, endPos); + } + + this.textarea.prop('selectionStart', cursorPos); + this.textarea.prop('selectionEnd', cursorPos); + } + + public saveChanges(): void { + this.save.emit({editedContent: this.content}); + } + + public discardChanges(): void { + this.discard.emit({}); + } + + public get currentEditMode(): EditMode { + return this.editMode; + } + + /** + * Insert text in such a way that the browser adds it to the 'undo' stack. This has different feature support + * depending on the platform. + */ + private insertText(text: string, startPos: number, endPos: number): void { + if (this.browserPlatform === 'firefox') { + // FIXME: Ctrl-Z highlights previous text + this.textarea.val(this.textarea.val().substr(0, startPos) + + text + + this.textarea.val().substr(endPos, this.textarea.val().length)); + } + else { + // TODO: Test other platforms (IE...) + this.$document[0].execCommand('insertText', false, text); + } + } +} + + +/** + * Type representing the current editing mode. + */ +export type EditMode = "write" | "preview"; diff --git a/static/js/directives/ui/markdown/markdown-input.component.css b/static/js/directives/ui/markdown/markdown-input.component.css new file mode 100644 index 000000000..d8a8e3a78 --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-input.component.css @@ -0,0 +1,14 @@ +.markdown-input-container .glyphicon-edit { + float: right; + color: #ddd; + transition: color 0.5s ease-in-out; +} + +.markdown-input-container .glyphicon-edit:hover { + cursor: pointer; + color: #444; +} + +.markdown-input-placeholder-editable:hover { + cursor: pointer; +} \ No newline at end of file diff --git a/static/js/directives/ui/markdown/markdown-input.component.html b/static/js/directives/ui/markdown/markdown-input.component.html new file mode 100644 index 000000000..c422e1d42 --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-input.component.html @@ -0,0 +1,29 @@ +
+
+ +
+ +
+ + + Click to set {{ ::$ctrl.fieldTitle }} + + + + No {{ ::$ctrl.fieldTitle }} has been set + +
+ + +
+ +
+
diff --git a/static/js/directives/ui/markdown/markdown-input.component.spec.ts b/static/js/directives/ui/markdown/markdown-input.component.spec.ts new file mode 100644 index 000000000..56b3ba697 --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-input.component.spec.ts @@ -0,0 +1,34 @@ +import { MarkdownInputComponent } from './markdown-input.component'; +import { Mock } from 'ts-mocks'; +import Spy = jasmine.Spy; + + +describe("MarkdownInputComponent", () => { + var component: MarkdownInputComponent; + + beforeEach(() => { + component = new MarkdownInputComponent(); + }); + + describe("editContent", () => { + + }); + + describe("saveContent", () => { + var editedContent: string; + + it("emits output event with changed content", (done) => { + editedContent = "# Some markdown here"; + component.contentChanged.subscribe((event: {content: string}) => { + expect(event.content).toEqual(editedContent); + done(); + }); + + component.saveContent({editedContent: editedContent}); + }); + }); + + describe("discardContent", () => { + + }); +}); diff --git a/static/js/directives/ui/markdown/markdown-input.component.ts b/static/js/directives/ui/markdown/markdown-input.component.ts new file mode 100644 index 000000000..69132feac --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-input.component.ts @@ -0,0 +1,32 @@ +import { Component, Input, Output, EventEmitter } from 'ng-metadata/core'; +import './markdown-input.component.css'; + + +/** + * Displays editable Markdown content. + */ +@Component({ + selector: 'markdown-input', + templateUrl: '/static/js/directives/ui/markdown/markdown-input.component.html' +}) +export class MarkdownInputComponent { + + @Input('<') public content: string; + @Input('<') public canWrite: boolean; + @Input('@') public fieldTitle: string; + @Output() public contentChanged: EventEmitter<{content: string}> = new EventEmitter(); + private isEditing: boolean = false; + + public editContent(): void { + this.isEditing = true; + } + + public saveContent(event: {editedContent: string}): void { + this.contentChanged.emit({content: event.editedContent}); + this.isEditing = false; + } + + public discardContent(event: any): void { + this.isEditing = false; + } +} diff --git a/static/js/directives/ui/markdown/markdown-toolbar.component.css b/static/js/directives/ui/markdown/markdown-toolbar.component.css new file mode 100644 index 000000000..f2521caa7 --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-toolbar.component.css @@ -0,0 +1,8 @@ +.markdown-toolbar-element .dropdown-menu li > * { + margin: 0 4px; +} + +.markdown-toolbar-element .dropdown-menu li:hover { + cursor: pointer; + background-color: #e6e6e6; +} diff --git a/static/js/directives/ui/markdown/markdown-toolbar.component.html b/static/js/directives/ui/markdown/markdown-toolbar.component.html new file mode 100644 index 000000000..6ad04dc71 --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-toolbar.component.html @@ -0,0 +1,61 @@ +
+ +
diff --git a/static/js/directives/ui/markdown/markdown-toolbar.component.spec.ts b/static/js/directives/ui/markdown/markdown-toolbar.component.spec.ts new file mode 100644 index 000000000..90d366d1f --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-toolbar.component.spec.ts @@ -0,0 +1,11 @@ +import { MarkdownToolbarComponent } from './markdown-toolbar.component'; +import { MarkdownSymbol } from '../../../types/common.types'; + + +describe("MarkdownToolbarComponent", () => { + var component: MarkdownToolbarComponent; + + beforeEach(() => { + component = new MarkdownToolbarComponent(); + }); +}); diff --git a/static/js/directives/ui/markdown/markdown-toolbar.component.ts b/static/js/directives/ui/markdown/markdown-toolbar.component.ts new file mode 100644 index 000000000..880c0abd3 --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-toolbar.component.ts @@ -0,0 +1,17 @@ +import { Component, Input, Output, EventEmitter } from 'ng-metadata/core'; +import { MarkdownSymbol } from '../../../types/common.types'; +import './markdown-toolbar.component.css'; + + +/** + * Toolbar containing Markdown symbol shortcuts. + */ +@Component({ + selector: 'markdown-toolbar', + templateUrl: '/static/js/directives/ui/markdown/markdown-toolbar.component.html' +}) +export class MarkdownToolbarComponent { + + @Input('<') public allowUndo: boolean = true; + @Output() public insertSymbol: EventEmitter<{symbol: MarkdownSymbol}> = new EventEmitter(); +} diff --git a/static/js/directives/ui/markdown/markdown-view.component.css b/static/js/directives/ui/markdown/markdown-view.component.css new file mode 100644 index 000000000..9cb3f601f --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-view.component.css @@ -0,0 +1,11 @@ +.markdown-view-content { + word-wrap: break-word; +} + +.markdown-view-content p { + margin: 0; +} + +code * { + font-family: "Lucida Console", Monaco, monospace; +} diff --git a/static/js/directives/ui/markdown/markdown-view.component.html b/static/js/directives/ui/markdown/markdown-view.component.html new file mode 100644 index 000000000..c3c164d4a --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-view.component.html @@ -0,0 +1,2 @@ +
diff --git a/static/js/directives/ui/markdown/markdown-view.component.spec.ts b/static/js/directives/ui/markdown/markdown-view.component.spec.ts new file mode 100644 index 000000000..e51d5bdf4 --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-view.component.spec.ts @@ -0,0 +1,81 @@ +import { MarkdownViewComponent } from './markdown-view.component'; +import { SimpleChanges } from 'ng-metadata/core'; +import { Converter, ConverterOptions } from 'showdown'; +import { Mock } from 'ts-mocks'; +import Spy = jasmine.Spy; + + +describe("MarkdownViewComponent", () => { + var component: MarkdownViewComponent; + var markdownConverterMock: Mock; + var $sceMock: Mock; + var $sanitizeMock: ng.sanitize.ISanitizeService; + + beforeEach(() => { + markdownConverterMock = new Mock(); + $sceMock = new Mock(); + $sanitizeMock = jasmine.createSpy('$sanitizeSpy').and.callFake((html: string) => html); + component = new MarkdownViewComponent((options: ConverterOptions) => markdownConverterMock.Object, + $sceMock.Object, + $sanitizeMock); + }); + + describe("ngOnChanges", () => { + var changes: SimpleChanges; + var markdown: string; + var expectedPlaceholder: string; + var markdownChars: string[]; + + beforeEach(() => { + changes = {}; + markdown = `## Heading\n Code line\n\n- Item\n> Quote\`code snippet\`\n\nThis is my project!`; + expectedPlaceholder = `

placeholder

`; + markdownChars = ['#', '-', '>', '`']; + markdownConverterMock.setup(mock => mock.makeHtml).is((text) => text); + $sceMock.setup(mock => mock.trustAsHtml).is((html) => html); + }); + + it("calls markdown converter to convert content to HTML when content is changed", () => { + changes['content'] = {currentValue: markdown, previousValue: '', isFirstChange: () => false}; + component.ngOnChanges(changes); + + expect((markdownConverterMock.Object.makeHtml).calls.argsFor(0)[0]).toEqual(changes['content'].currentValue); + }); + + it("only converts first line of content to HTML if flag is set when content is changed", () => { + component.firstLineOnly = true; + changes['content'] = {currentValue: markdown, previousValue: '', isFirstChange: () => false}; + component.ngOnChanges(changes); + + const expectedHtml: string = markdown.split('\n') + .filter(line => line.indexOf(' ') != 0) + .filter(line => line.trim().length != 0) + .filter(line => markdownChars.indexOf(line.trim()[0]) == -1)[0]; + + expect((markdownConverterMock.Object.makeHtml).calls.argsFor(0)[0]).toEqual(expectedHtml); + }); + + it("sets converted HTML to be a placeholder if flag is set and content is empty", () => { + component.placeholderNeeded = true; + changes['content'] = {currentValue: '', previousValue: '', isFirstChange: () => false}; + component.ngOnChanges(changes); + + expect((markdownConverterMock.Object.makeHtml)).not.toHaveBeenCalled(); + expect(($sceMock.Object.trustAsHtml).calls.argsFor(0)[0]).toEqual(expectedPlaceholder); + }); + + it("sets converted HTML to empty string if placeholder flag is false and content is empty", () => { + changes['content'] = {currentValue: '', previousValue: '', isFirstChange: () => false}; + component.ngOnChanges(changes); + + expect((markdownConverterMock.Object.makeHtml).calls.argsFor(0)[0]).toEqual(changes['content'].currentValue); + }); + + it("calls $sanitize service to sanitize changed HTML content", () => { + changes['content'] = {currentValue: markdown, previousValue: '', isFirstChange: () => false}; + component.ngOnChanges(changes); + + expect(($sanitizeMock).calls.argsFor(0)[0]).toEqual(changes['content'].currentValue); + }); + }); +}); diff --git a/static/js/directives/ui/markdown/markdown-view.component.ts b/static/js/directives/ui/markdown/markdown-view.component.ts new file mode 100644 index 000000000..b694ce943 --- /dev/null +++ b/static/js/directives/ui/markdown/markdown-view.component.ts @@ -0,0 +1,51 @@ +import { Component, Input, Inject, OnChanges, SimpleChanges } from 'ng-metadata/core'; +import { Converter, ConverterOptions } from 'showdown'; +import 'showdown-highlightjs-extension'; +import 'highlightjs/styles/vs.css'; +import './markdown-view.component.css'; + + +/** + * Renders Markdown content to HTML. + */ +@Component({ + selector: 'markdown-view', + templateUrl: '/static/js/directives/ui/markdown/markdown-view.component.html' +}) +export class MarkdownViewComponent implements OnChanges { + + @Input('<') public content: string; + @Input('<') public firstLineOnly: boolean = false; + @Input('<') public placeholderNeeded: boolean = false; + private convertedHTML: string = ''; + private readonly placeholder: string = `

placeholder

`; + private readonly markdownChars: string[] = ['#', '-', '>', '`']; + private markdownConverter: Converter; + + constructor(@Inject('markdownConverterFactory') private makeConverter: (options?: ConverterOptions) => Converter, + @Inject('$sce') private $sce: ng.ISCEService, + @Inject('$sanitize') private $sanitize: ng.sanitize.ISanitizeService) { + this.markdownConverter = makeConverter({extensions: ['highlightjs']}); + } + + public ngOnChanges(changes: SimpleChanges): void { + if (changes['content']) { + if (!changes['content'].currentValue && this.placeholderNeeded) { + this.convertedHTML = this.$sce.trustAsHtml(this.placeholder); + } else if (this.firstLineOnly) { + const firstLine: string = changes['content'].currentValue.split('\n') + // Skip code lines + .filter(line => line.indexOf(' ') != 0) + // Skip empty lines + .filter(line => line.trim().length != 0) + // Skip control lines + .filter(line => this.markdownChars.indexOf(line.trim()[0]) == -1)[0]; + + this.convertedHTML = this.$sanitize(this.markdownConverter.makeHtml(firstLine)); + } else { + this.convertedHTML = this.$sanitize(this.markdownConverter.makeHtml(changes['content'].currentValue)); + } + + } + } +} diff --git a/static/js/directives/ui/search-box/search-box.component.html b/static/js/directives/ui/search-box/search-box.component.html index 162529c52..69495614a 100644 --- a/static/js/directives/ui/search-box/search-box.component.html +++ b/static/js/directives/ui/search-box/search-box.component.html @@ -39,8 +39,11 @@ {{ result.namespace.name }}/{{ result.name }}
-
+
+ +
diff --git a/static/js/pages/new-repo.js b/static/js/pages/new-repo.js index 18f63ac26..8821b69e0 100644 --- a/static/js/pages/new-repo.js +++ b/static/js/pages/new-repo.js @@ -48,6 +48,10 @@ $scope.startBuildCallback = startBuild; }; + $scope.updateDescription = function(content) { + $scope.repo.description = content; + }; + $scope.createNewRepo = function() { $scope.creating = true; var repo = $scope.repo; diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts index 0e3509faf..f3d13681f 100644 --- a/static/js/quay.module.ts +++ b/static/js/quay.module.ts @@ -32,6 +32,12 @@ import { DockerfileServiceImpl } from './services/dockerfile/dockerfile.service. import { DataFileServiceImpl } from './services/datafile/datafile.service.impl'; import { UtilServiceImpl } from './services/util/util.service.impl'; import { QuayRequireDirective } from './directives/structural/quay-require/quay-require.directive'; +import { MarkdownInputComponent } from './directives/ui/markdown/markdown-input.component'; +import { MarkdownViewComponent } from './directives/ui/markdown/markdown-view.component'; +import { MarkdownToolbarComponent } from './directives/ui/markdown/markdown-toolbar.component'; +import { MarkdownEditorComponent } from './directives/ui/markdown/markdown-editor.component'; +import { BrowserPlatform, browserPlatform } from './constants/platform.constant'; +import { Converter, ConverterOptions } from 'showdown'; /** @@ -60,6 +66,10 @@ import { QuayRequireDirective } from './directives/structural/quay-require/quay- RepositorySigningConfigComponent, TimeMachineSettingsComponent, DurationInputComponent, + MarkdownInputComponent, + MarkdownViewComponent, + MarkdownToolbarComponent, + MarkdownEditorComponent, SearchBoxComponent, TypeaheadDirective, CorTabPanelComponent, @@ -74,8 +84,9 @@ import { QuayRequireDirective } from './directives/structural/quay-require/quay- AvatarServiceImpl, DockerfileServiceImpl, DataFileServiceImpl, - UtilServiceImpl, {provide: 'fileReaderFactory', useValue: () => new FileReader()}, + {provide: 'markdownConverterFactory', useValue: (options?: ConverterOptions) => new Converter(options)}, + {provide: 'BrowserPlatform', useValue: browserPlatform}, {provide: 'CorTabCurrentHandlerFactory', useValue: CorTabCurrentHandlerFactory}, ], }) diff --git a/static/js/types/common.types.ts b/static/js/types/common.types.ts index 803f43595..f3331cc71 100644 --- a/static/js/types/common.types.ts +++ b/static/js/types/common.types.ts @@ -104,6 +104,7 @@ export type Trigger = { service: any; }; + /** * Represents an apostille signature document, with extra expiration information. */ @@ -118,6 +119,7 @@ export type ApostilleSignatureDocument = { error: boolean }; + /** * An apostille document containing signatures for a tag. */ @@ -127,4 +129,20 @@ export type ApostilleTagDocument = { // The hashes for the tag. hashes: {string: string} -}; \ No newline at end of file +}; + + +/** + * A type representing a Markdown symbol. + */ +export type MarkdownSymbol = 'heading1' + | 'heading2' + | 'heading3' + | 'bold' + | 'italics' + | 'bulleted-list' + | 'numbered-list' + | 'quote' + | 'code' + | 'link' + | 'code'; diff --git a/static/lib/pagedown/LICENSE.txt b/static/lib/pagedown/LICENSE.txt deleted file mode 100755 index ca48b4f78..000000000 --- a/static/lib/pagedown/LICENSE.txt +++ /dev/null @@ -1,32 +0,0 @@ -A javascript port of Markdown, as used on Stack Overflow -and the rest of Stack Exchange network. - -Largely based on showdown.js by John Fraser (Attacklab). - -Original Markdown Copyright (c) 2004-2005 John Gruber - - - -Original Showdown code copyright (c) 2007 John Fraser - -Modifications and bugfixes (c) 2009 Dana Robinson -Modifications and bugfixes (c) 2009-2011 Stack Exchange Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - diff --git a/static/lib/pagedown/Markdown.Converter.js b/static/lib/pagedown/Markdown.Converter.js deleted file mode 100755 index 308497337..000000000 --- a/static/lib/pagedown/Markdown.Converter.js +++ /dev/null @@ -1,1346 +0,0 @@ -var Markdown; - -if (typeof exports === "object" && typeof require === "function") // we're in a CommonJS (e.g. Node.js) module - Markdown = exports; -else - Markdown = {}; - -window.Markdown = Markdown; - -// The following text is included for historical reasons, but should -// be taken with a pinch of salt; it's not all true anymore. - -// -// Wherever possible, Showdown is a straight, line-by-line port -// of the Perl version of Markdown. -// -// This is not a normal parser design; it's basically just a -// series of string substitutions. It's hard to read and -// maintain this way, but keeping Showdown close to the original -// design makes it easier to port new features. -// -// More importantly, Showdown behaves like markdown.pl in most -// edge cases. So web applications can do client-side preview -// in Javascript, and then build identical HTML on the server. -// -// This port needs the new RegExp functionality of ECMA 262, -// 3rd Edition (i.e. Javascript 1.5). Most modern web browsers -// should do fine. Even with the new regular expression features, -// We do a lot of work to emulate Perl's regex functionality. -// The tricky changes in this file mostly have the "attacklab:" -// label. Major or self-explanatory changes don't. -// -// Smart diff tools like Araxis Merge will be able to match up -// this file with markdown.pl in a useful way. A little tweaking -// helps: in a copy of markdown.pl, replace "#" with "//" and -// replace "$text" with "text". Be sure to ignore whitespace -// and line endings. -// - - -// -// Usage: -// -// var text = "Markdown *rocks*."; -// -// var converter = new Markdown.Converter(); -// var html = converter.makeHtml(text); -// -// alert(html); -// -// Note: move the sample code to the bottom of this -// file before uncommenting it. -// - -(function () { - - function identity(x) { return x; } - function returnFalse(x) { return false; } - - function HookCollection() { } - - HookCollection.prototype = { - - chain: function (hookname, func) { - var original = this[hookname]; - if (!original) - throw new Error("unknown hook " + hookname); - - if (original === identity) - this[hookname] = func; - else - this[hookname] = function (x) { return func(original(x)); } - }, - set: function (hookname, func) { - if (!this[hookname]) - throw new Error("unknown hook " + hookname); - this[hookname] = func; - }, - addNoop: function (hookname) { - this[hookname] = identity; - }, - addFalse: function (hookname) { - this[hookname] = returnFalse; - } - }; - - Markdown.HookCollection = HookCollection; - - // g_urls and g_titles allow arbitrary user-entered strings as keys. This - // caused an exception (and hence stopped the rendering) when the user entered - // e.g. [push] or [__proto__]. Adding a prefix to the actual key prevents this - // (since no builtin property starts with "s_"). See - // http://meta.stackoverflow.com/questions/64655/strange-wmd-bug - // (granted, switching from Array() to Object() alone would have left only __proto__ - // to be a problem) - function SaveHash() { } - SaveHash.prototype = { - set: function (key, value) { - this["s_" + key] = value; - }, - get: function (key) { - return this["s_" + key]; - } - }; - - Markdown.Converter = function () { - var pluginHooks = this.hooks = new HookCollection(); - pluginHooks.addNoop("plainLinkText"); // given a URL that was encountered by itself (without markup), should return the link text that's to be given to this link - pluginHooks.addNoop("preConversion"); // called with the orignal text as given to makeHtml. The result of this plugin hook is the actual markdown source that will be cooked - pluginHooks.addNoop("postConversion"); // called with the final cooked HTML code. The result of this plugin hook is the actual output of makeHtml - - // - // Private state of the converter instance: - // - - // Global hashes, used by various utility routines - var g_urls; - var g_titles; - var g_html_blocks; - - // Used to track when we're inside an ordered or unordered list - // (see _ProcessListItems() for details): - var g_list_level; - - this.makeHtml = function (text) { - - // - // Main function. The order in which other subs are called here is - // essential. Link and image substitutions need to happen before - // _EscapeSpecialCharsWithinTagAttributes(), so that any *'s or _'s in the - // and tags get encoded. - // - - // This will only happen if makeHtml on the same converter instance is called from a plugin hook. - // Don't do that. - if (g_urls) - throw new Error("Recursive call to converter.makeHtml"); - - // Create the private state objects. - g_urls = new SaveHash(); - g_titles = new SaveHash(); - g_html_blocks = []; - g_list_level = 0; - - text = pluginHooks.preConversion(text); - - // attacklab: Replace ~ with ~T - // This lets us use tilde as an escape char to avoid md5 hashes - // The choice of character is arbitray; anything that isn't - // magic in Markdown will work. - text = text.replace(/~/g, "~T"); - - // attacklab: Replace $ with ~D - // RegExp interprets $ as a special character - // when it's in a replacement string - text = text.replace(/\$/g, "~D"); - - // Standardize line endings - text = text.replace(/\r\n/g, "\n"); // DOS to Unix - text = text.replace(/\r/g, "\n"); // Mac to Unix - - // Make sure text begins and ends with a couple of newlines: - text = "\n\n" + text + "\n\n"; - - // Convert all tabs to spaces. - text = _Detab(text); - - // Strip any lines consisting only of spaces and tabs. - // This makes subsequent regexen easier to write, because we can - // match consecutive blank lines with /\n+/ instead of something - // contorted like /[ \t]*\n+/ . - text = text.replace(/^[ \t]+$/mg, ""); - - // Turn block-level HTML blocks into hash entries - text = _HashHTMLBlocks(text); - - // Strip link definitions, store in hashes. - text = _StripLinkDefinitions(text); - - text = _RunBlockGamut(text); - - text = _UnescapeSpecialChars(text); - - // attacklab: Restore dollar signs - text = text.replace(/~D/g, "$$"); - - // attacklab: Restore tildes - text = text.replace(/~T/g, "~"); - - text = pluginHooks.postConversion(text); - - g_html_blocks = g_titles = g_urls = null; - - return text; - }; - - function _StripLinkDefinitions(text) { - // - // Strips link definitions from text, stores the URLs and titles in - // hash references. - // - - // Link defs are in the form: ^[id]: url "optional title" - - /* - text = text.replace(/ - ^[ ]{0,3}\[(.+)\]: // id = $1 attacklab: g_tab_width - 1 - [ \t]* - \n? // maybe *one* newline - [ \t]* - ? // url = $2 - (?=\s|$) // lookahead for whitespace instead of the lookbehind removed below - [ \t]* - \n? // maybe one newline - [ \t]* - ( // (potential) title = $3 - (\n*) // any lines skipped = $4 attacklab: lookbehind removed - [ \t]+ - ["(] - (.+?) // title = $5 - [")] - [ \t]* - )? // title is optional - (?:\n+|$) - /gm, function(){...}); - */ - - text = text.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*?(?=\s|$)[ \t]*\n?[ \t]*((\n*)["(](.+?)[")][ \t]*)?(?:\n+)/gm, - function (wholeMatch, m1, m2, m3, m4, m5) { - m1 = m1.toLowerCase(); - g_urls.set(m1, _EncodeAmpsAndAngles(m2)); // Link IDs are case-insensitive - if (m4) { - // Oops, found blank lines, so it's not a title. - // Put back the parenthetical statement we stole. - return m3; - } else if (m5) { - g_titles.set(m1, m5.replace(/"/g, """)); - } - - // Completely remove the definition from the text - return ""; - } - ); - - return text; - } - - function _HashHTMLBlocks(text) { - - // Hashify HTML blocks: - // We only want to do this for block-level HTML tags, such as headers, - // lists, and tables. That's because we still want to wrap

s around - // "paragraphs" that are wrapped in non-block-level tags, such as anchors, - // phrase emphasis, and spans. The list of tags we're looking for is - // hard-coded: - var block_tags_a = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del" - var block_tags_b = "p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math" - - // First, look for nested blocks, e.g.: - //

- //
- // tags for inner block must be indented. - //
- //
- // - // The outermost tags must start at the left margin for this to match, and - // the inner nested divs must be indented. - // We need to do this before the next, more liberal match, because the next - // match will start at the first `
` and stop at the first `
`. - - // attacklab: This regex can be expensive when it fails. - - /* - text = text.replace(/ - ( // save in $1 - ^ // start of line (with /m) - <($block_tags_a) // start tag = $2 - \b // word break - // attacklab: hack around khtml/pcre bug... - [^\r]*?\n // any number of lines, minimally matching - // the matching end tag - [ \t]* // trailing spaces/tabs - (?=\n+) // followed by a newline - ) // attacklab: there are sentinel newlines at end of document - /gm,function(){...}}; - */ - text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm, hashElement); - - // - // Now match more liberally, simply from `\n` to `\n` - // - - /* - text = text.replace(/ - ( // save in $1 - ^ // start of line (with /m) - <($block_tags_b) // start tag = $2 - \b // word break - // attacklab: hack around khtml/pcre bug... - [^\r]*? // any number of lines, minimally matching - .* // the matching end tag - [ \t]* // trailing spaces/tabs - (?=\n+) // followed by a newline - ) // attacklab: there are sentinel newlines at end of document - /gm,function(){...}}; - */ - text = text.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm, hashElement); - - // Special case just for
. It was easier to make a special case than - // to make the other regex more complicated. - - /* - text = text.replace(/ - \n // Starting after a blank line - [ ]{0,3} - ( // save in $1 - (<(hr) // start tag = $2 - \b // word break - ([^<>])*? - \/?>) // the matching end tag - [ \t]* - (?=\n{2,}) // followed by a blank line - ) - /g,hashElement); - */ - text = text.replace(/\n[ ]{0,3}((<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g, hashElement); - - // Special case for standalone HTML comments: - - /* - text = text.replace(/ - \n\n // Starting after a blank line - [ ]{0,3} // attacklab: g_tab_width - 1 - ( // save in $1 - -]|-[^>])(?:[^-]|-[^-])*)--) // see http://www.w3.org/TR/html-markup/syntax.html#comments and http://meta.stackoverflow.com/q/95256 - > - [ \t]* - (?=\n{2,}) // followed by a blank line - ) - /g,hashElement); - */ - text = text.replace(/\n\n[ ]{0,3}(-]|-[^>])(?:[^-]|-[^-])*)--)>[ \t]*(?=\n{2,}))/g, hashElement); - - // PHP and ASP-style processor instructions ( and <%...%>) - - /* - text = text.replace(/ - (?: - \n\n // Starting after a blank line - ) - ( // save in $1 - [ ]{0,3} // attacklab: g_tab_width - 1 - (?: - <([?%]) // $2 - [^\r]*? - \2> - ) - [ \t]* - (?=\n{2,}) // followed by a blank line - ) - /g,hashElement); - */ - text = text.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g, hashElement); - - return text; - } - - function hashElement(wholeMatch, m1) { - var blockText = m1; - - // Undo double lines - blockText = blockText.replace(/^\n+/, ""); - - // strip trailing blank lines - blockText = blockText.replace(/\n+$/g, ""); - - // Replace the element text with a marker ("~KxK" where x is its key) - blockText = "\n\n~K" + (g_html_blocks.push(blockText) - 1) + "K\n\n"; - - return blockText; - } - - function _RunBlockGamut(text, doNotUnhash) { - // - // These are all the transformations that form block-level - // tags like paragraphs, headers, and list items. - // - text = _DoHeaders(text); - - // Do Horizontal Rules: - var replacement = "
\n"; - text = text.replace(/^[ ]{0,2}([ ]?\*[ ]?){3,}[ \t]*$/gm, replacement); - text = text.replace(/^[ ]{0,2}([ ]?-[ ]?){3,}[ \t]*$/gm, replacement); - text = text.replace(/^[ ]{0,2}([ ]?_[ ]?){3,}[ \t]*$/gm, replacement); - - text = _DoLists(text); - text = _DoCodeBlocks(text); - text = _DoBlockQuotes(text); - - // We already ran _HashHTMLBlocks() before, in Markdown(), but that - // was to escape raw HTML in the original Markdown source. This time, - // we're escaping the markup we've just created, so that we don't wrap - //

tags around block-level tags. - text = _HashHTMLBlocks(text); - text = _FormParagraphs(text, doNotUnhash); - - return text; - } - - function _RunSpanGamut(text) { - // - // These are all the transformations that occur *within* block-level - // tags like paragraphs, headers, and list items. - // - - text = _DoCodeSpans(text); - text = _EscapeSpecialCharsWithinTagAttributes(text); - text = _EncodeBackslashEscapes(text); - - // Process anchor and image tags. Images must come first, - // because ![foo][f] looks like an anchor. - text = _DoImages(text); - text = _DoAnchors(text); - - // Make links out of things like `` - // Must come after _DoAnchors(), because you can use < and > - // delimiters in inline links like [this](). - text = _DoAutoLinks(text); - - text = text.replace(/~P/g, "://"); // put in place to prevent autolinking; reset now - - text = _EncodeAmpsAndAngles(text); - text = _DoItalicsAndBold(text); - - // Do hard breaks: - text = text.replace(/ +\n/g, "
\n"); - - return text; - } - - function _EscapeSpecialCharsWithinTagAttributes(text) { - // - // Within tags -- meaning between < and > -- encode [\ ` * _] so they - // don't conflict with their use in Markdown for code, italics and strong. - // - - // Build a regex to find HTML tags and comments. See Friedl's - // "Mastering Regular Expressions", 2nd Ed., pp. 200-201. - - // SE: changed the comment part of the regex - - var regex = /(<[a-z\/!$]("[^"]*"|'[^']*'|[^'">])*>|-]|-[^>])(?:[^-]|-[^-])*)--)>)/gi; - - text = text.replace(regex, function (wholeMatch) { - var tag = wholeMatch.replace(/(.)<\/?code>(?=.)/g, "$1`"); - tag = escapeCharacters(tag, wholeMatch.charAt(1) == "!" ? "\\`*_/" : "\\`*_"); // also escape slashes in comments to prevent autolinking there -- http://meta.stackoverflow.com/questions/95987 - return tag; - }); - - return text; - } - - function _DoAnchors(text) { - // - // Turn Markdown link shortcuts into XHTML
tags. - // - // - // First, handle reference-style links: [link text] [id] - // - - /* - text = text.replace(/ - ( // wrap whole match in $1 - \[ - ( - (?: - \[[^\]]*\] // allow brackets nested one level - | - [^\[] // or anything else - )* - ) - \] - - [ ]? // one optional space - (?:\n[ ]*)? // one optional newline followed by spaces - - \[ - (.*?) // id = $3 - \] - ) - ()()()() // pad remaining backreferences - /g, writeAnchorTag); - */ - text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeAnchorTag); - - // - // Next, inline-style links: [link text](url "optional title") - // - - /* - text = text.replace(/ - ( // wrap whole match in $1 - \[ - ( - (?: - \[[^\]]*\] // allow brackets nested one level - | - [^\[\]] // or anything else - )* - ) - \] - \( // literal paren - [ \t]* - () // no id, so leave $3 empty - ? - [ \t]* - ( // $5 - (['"]) // quote char = $6 - (.*?) // Title = $7 - \6 // matching quote - [ \t]* // ignore any spaces/tabs between closing quote and ) - )? // title is optional - \) - ) - /g, writeAnchorTag); - */ - - text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeAnchorTag); - - // - // Last, handle reference-style shortcuts: [link text] - // These must come last in case you've also got [link test][1] - // or [link test](/foo) - // - - /* - text = text.replace(/ - ( // wrap whole match in $1 - \[ - ([^\[\]]+) // link text = $2; can't contain '[' or ']' - \] - ) - ()()()()() // pad rest of backreferences - /g, writeAnchorTag); - */ - text = text.replace(/(\[([^\[\]]+)\])()()()()()/g, writeAnchorTag); - - return text; - } - - function writeAnchorTag(wholeMatch, m1, m2, m3, m4, m5, m6, m7) { - if (m7 == undefined) m7 = ""; - var whole_match = m1; - var link_text = m2.replace(/:\/\//g, "~P"); // to prevent auto-linking withing the link. will be converted back after the auto-linker runs - var link_id = m3.toLowerCase(); - var url = m4; - var title = m7; - - if (url == "") { - if (link_id == "") { - // lower-case and turn embedded newlines into spaces - link_id = link_text.toLowerCase().replace(/ ?\n/g, " "); - } - url = "#" + link_id; - - if (g_urls.get(link_id) != undefined) { - url = g_urls.get(link_id); - if (g_titles.get(link_id) != undefined) { - title = g_titles.get(link_id); - } - } - else { - if (whole_match.search(/\(\s*\)$/m) > -1) { - // Special case for explicit empty url - url = ""; - } else { - return whole_match; - } - } - } - url = encodeProblemUrlChars(url); - url = escapeCharacters(url, "*_"); - var result = ""; - - return result; - } - - function _DoImages(text) { - // - // Turn Markdown image shortcuts into tags. - // - - // - // First, handle reference-style labeled images: ![alt text][id] - // - - /* - text = text.replace(/ - ( // wrap whole match in $1 - !\[ - (.*?) // alt text = $2 - \] - - [ ]? // one optional space - (?:\n[ ]*)? // one optional newline followed by spaces - - \[ - (.*?) // id = $3 - \] - ) - ()()()() // pad rest of backreferences - /g, writeImageTag); - */ - text = text.replace(/(!\[(.*?)\][ ]?(?:\n[ ]*)?\[(.*?)\])()()()()/g, writeImageTag); - - // - // Next, handle inline images: ![alt text](url "optional title") - // Don't forget: encode * and _ - - /* - text = text.replace(/ - ( // wrap whole match in $1 - !\[ - (.*?) // alt text = $2 - \] - \s? // One optional whitespace character - \( // literal paren - [ \t]* - () // no id, so leave $3 empty - ? // src url = $4 - [ \t]* - ( // $5 - (['"]) // quote char = $6 - (.*?) // title = $7 - \6 // matching quote - [ \t]* - )? // title is optional - \) - ) - /g, writeImageTag); - */ - text = text.replace(/(!\[(.*?)\]\s?\([ \t]*()?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g, writeImageTag); - - return text; - } - - function attributeEncode(text) { - // unconditionally replace angle brackets here -- what ends up in an attribute (e.g. alt or title) - // never makes sense to have verbatim HTML in it (and the sanitizer would totally break it) - return text.replace(/>/g, ">").replace(/" + _RunSpanGamut(m1) + "\n\n"; } - ); - - text = text.replace(/^(.+)[ \t]*\n-+[ \t]*\n+/gm, - function (matchFound, m1) { return "

" + _RunSpanGamut(m1) + "

\n\n"; } - ); - - // atx-style headers: - // # Header 1 - // ## Header 2 - // ## Header 2 with closing hashes ## - // ... - // ###### Header 6 - // - - /* - text = text.replace(/ - ^(\#{1,6}) // $1 = string of #'s - [ \t]* - (.+?) // $2 = Header text - [ \t]* - \#* // optional closing #'s (not counted) - \n+ - /gm, function() {...}); - */ - - text = text.replace(/^(\#{1,6})[ \t]*(.+?)[ \t]*\#*\n+/gm, - function (wholeMatch, m1, m2) { - var h_level = m1.length; - return "" + _RunSpanGamut(m2) + "\n\n"; - } - ); - - return text; - } - - function _DoLists(text) { - // - // Form HTML ordered (numbered) and unordered (bulleted) lists. - // - - // attacklab: add sentinel to hack around khtml/safari bug: - // http://bugs.webkit.org/show_bug.cgi?id=11231 - text += "~0"; - - // Re-usable pattern to match any entirel ul or ol list: - - /* - var whole_list = / - ( // $1 = whole list - ( // $2 - [ ]{0,3} // attacklab: g_tab_width - 1 - ([*+-]|\d+[.]) // $3 = first list item marker - [ \t]+ - ) - [^\r]+? - ( // $4 - ~0 // sentinel for workaround; should be $ - | - \n{2,} - (?=\S) - (?! // Negative lookahead for another list item marker - [ \t]* - (?:[*+-]|\d+[.])[ \t]+ - ) - ) - ) - /g - */ - var whole_list = /^(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm; - - if (g_list_level) { - text = text.replace(whole_list, function (wholeMatch, m1, m2) { - var list = m1; - var list_type = (m2.search(/[*+-]/g) > -1) ? "ul" : "ol"; - - var result = _ProcessListItems(list, list_type); - - // Trim any trailing whitespace, to put the closing `` - // up on the preceding line, to get it past the current stupid - // HTML block parser. This is a hack to work around the terrible - // hack that is the HTML block parser. - result = result.replace(/\s+$/, ""); - result = "<" + list_type + ">" + result + "\n"; - return result; - }); - } else { - whole_list = /(\n\n|^\n?)(([ ]{0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(~0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/g; - text = text.replace(whole_list, function (wholeMatch, m1, m2, m3) { - var runup = m1; - var list = m2; - - var list_type = (m3.search(/[*+-]/g) > -1) ? "ul" : "ol"; - var result = _ProcessListItems(list, list_type); - result = runup + "<" + list_type + ">\n" + result + "\n"; - return result; - }); - } - - // attacklab: strip sentinel - text = text.replace(/~0/, ""); - - return text; - } - - var _listItemMarkers = { ol: "\\d+[.]", ul: "[*+-]" }; - - function _ProcessListItems(list_str, list_type) { - // - // Process the contents of a single ordered or unordered list, splitting it - // into individual list items. - // - // list_type is either "ul" or "ol". - - // The $g_list_level global keeps track of when we're inside a list. - // Each time we enter a list, we increment it; when we leave a list, - // we decrement. If it's zero, we're not in a list anymore. - // - // We do this because when we're not inside a list, we want to treat - // something like this: - // - // I recommend upgrading to version - // 8. Oops, now this line is treated - // as a sub-list. - // - // As a single paragraph, despite the fact that the second line starts - // with a digit-period-space sequence. - // - // Whereas when we're inside a list (or sub-list), that line will be - // treated as the start of a sub-list. What a kludge, huh? This is - // an aspect of Markdown's syntax that's hard to parse perfectly - // without resorting to mind-reading. Perhaps the solution is to - // change the syntax rules such that sub-lists must start with a - // starting cardinal number; e.g. "1." or "a.". - - g_list_level++; - - // trim trailing blank lines: - list_str = list_str.replace(/\n{2,}$/, "\n"); - - // attacklab: add sentinel to emulate \z - list_str += "~0"; - - // In the original attacklab showdown, list_type was not given to this function, and anything - // that matched /[*+-]|\d+[.]/ would just create the next
  • , causing this mismatch: - // - // Markdown rendered by WMD rendered by MarkdownSharp - // ------------------------------------------------------------------ - // 1. first 1. first 1. first - // 2. second 2. second 2. second - // - third 3. third * third - // - // We changed this to behave identical to MarkdownSharp. This is the constructed RegEx, - // with {MARKER} being one of \d+[.] or [*+-], depending on list_type: - - /* - list_str = list_str.replace(/ - (^[ \t]*) // leading whitespace = $1 - ({MARKER}) [ \t]+ // list marker = $2 - ([^\r]+? // list item text = $3 - (\n+) - ) - (?= - (~0 | \2 ({MARKER}) [ \t]+) - ) - /gm, function(){...}); - */ - - var marker = _listItemMarkers[list_type]; - var re = new RegExp("(^[ \\t]*)(" + marker + ")[ \\t]+([^\\r]+?(\\n+))(?=(~0|\\1(" + marker + ")[ \\t]+))", "gm"); - var last_item_had_a_double_newline = false; - list_str = list_str.replace(re, - function (wholeMatch, m1, m2, m3) { - var item = m3; - var leading_space = m1; - var ends_with_double_newline = /\n\n$/.test(item); - var contains_double_newline = ends_with_double_newline || item.search(/\n{2,}/) > -1; - - if (contains_double_newline || last_item_had_a_double_newline) { - item = _RunBlockGamut(_Outdent(item), /* doNotUnhash = */true); - } - else { - // Recursion for sub-lists: - item = _DoLists(_Outdent(item)); - item = item.replace(/\n$/, ""); // chomp(item) - item = _RunSpanGamut(item); - } - last_item_had_a_double_newline = ends_with_double_newline; - return "
  • " + item + "
  • \n"; - } - ); - - // attacklab: strip sentinel - list_str = list_str.replace(/~0/g, ""); - - g_list_level--; - return list_str; - } - - function _DoCodeBlocks(text) { - // - // Process Markdown `
    ` blocks.
    -            //  
    -
    -            /*
    -            text = text.replace(/
    -                (?:\n\n|^)
    -                (                               // $1 = the code block -- one or more lines, starting with a space/tab
    -                    (?:
    -                        (?:[ ]{4}|\t)           // Lines must start with a tab or a tab-width of spaces - attacklab: g_tab_width
    -                        .*\n+
    -                    )+
    -                )
    -                (\n*[ ]{0,3}[^ \t\n]|(?=~0))    // attacklab: g_tab_width
    -            /g ,function(){...});
    -            */
    -
    -            // attacklab: sentinel workarounds for lack of \A and \Z, safari\khtml bug
    -            text += "~0";
    -
    -            text = text.replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=~0))/g,
    -                function (wholeMatch, m1, m2) {
    -                    var codeblock = m1;
    -                    var nextChar = m2;
    -
    -                    codeblock = _EncodeCode(_Outdent(codeblock));
    -                    codeblock = _Detab(codeblock);
    -                    codeblock = codeblock.replace(/^\n+/g, ""); // trim leading newlines
    -                    codeblock = codeblock.replace(/\n+$/g, ""); // trim trailing whitespace
    -
    -                    codeblock = '
    ' + codeblock + '\n
    '; - - return "\n\n" + codeblock + "\n\n" + nextChar; - } - ); - - // attacklab: strip sentinel - text = text.replace(/~0/, ""); - - return text; - } - - function hashBlock(text) { - text = text.replace(/(^\n+|\n+$)/g, ""); - return "\n\n~K" + (g_html_blocks.push(text) - 1) + "K\n\n"; - } - - function _DoCodeSpans(text) { - // - // * Backtick quotes are used for spans. - // - // * You can use multiple backticks as the delimiters if you want to - // include literal backticks in the code span. So, this input: - // - // Just type ``foo `bar` baz`` at the prompt. - // - // Will translate to: - // - //

    Just type foo `bar` baz at the prompt.

    - // - // There's no arbitrary limit to the number of backticks you - // can use as delimters. If you need three consecutive backticks - // in your code, use four for delimiters, etc. - // - // * You can use spaces to get literal backticks at the edges: - // - // ... type `` `bar` `` ... - // - // Turns to: - // - // ... type `bar` ... - // - - /* - text = text.replace(/ - (^|[^\\]) // Character before opening ` can't be a backslash - (`+) // $2 = Opening run of ` - ( // $3 = The code block - [^\r]*? - [^`] // attacklab: work around lack of lookbehind - ) - \2 // Matching closer - (?!`) - /gm, function(){...}); - */ - - text = text.replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm, - function (wholeMatch, m1, m2, m3, m4) { - var c = m3; - c = c.replace(/^([ \t]*)/g, ""); // leading whitespace - c = c.replace(/[ \t]*$/g, ""); // trailing whitespace - c = _EncodeCode(c); - c = c.replace(/:\/\//g, "~P"); // to prevent auto-linking. Not necessary in code *blocks*, but in code spans. Will be converted back after the auto-linker runs. - return m1 + "" + c + ""; - } - ); - - return text; - } - - function _EncodeCode(text) { - // - // Encode/escape certain characters inside Markdown code runs. - // The point is that in code, these characters are literals, - // and lose their special Markdown meanings. - // - // Encode all ampersands; HTML entities are not - // entities within a Markdown code span. - text = text.replace(/&/g, "&"); - - // Do the angle bracket song and dance: - text = text.replace(//g, ">"); - - // Now, escape characters that are magic in Markdown: - text = escapeCharacters(text, "\*_{}[]\\", false); - - // jj the line above breaks this: - //--- - - //* Item - - // 1. Subitem - - // special char: * - //--- - - return text; - } - - function _DoItalicsAndBold(text) { - - // must go first: - text = text.replace(/([\W_]|^)(\*\*|__)(?=\S)([^\r]*?\S[\*_]*)\2([\W_]|$)/g, - "$1$3$4"); - - text = text.replace(/([\W_]|^)(\*|_)(?=\S)([^\r\*_]*?\S)\2([\W_]|$)/g, - "$1$3$4"); - - return text; - } - - function _DoBlockQuotes(text) { - - /* - text = text.replace(/ - ( // Wrap whole match in $1 - ( - ^[ \t]*>[ \t]? // '>' at the start of a line - .+\n // rest of the first line - (.+\n)* // subsequent consecutive lines - \n* // blanks - )+ - ) - /gm, function(){...}); - */ - - text = text.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm, - function (wholeMatch, m1) { - var bq = m1; - - // attacklab: hack around Konqueror 3.5.4 bug: - // "----------bug".replace(/^-/g,"") == "bug" - - bq = bq.replace(/^[ \t]*>[ \t]?/gm, "~0"); // trim one level of quoting - - // attacklab: clean up hack - bq = bq.replace(/~0/g, ""); - - bq = bq.replace(/^[ \t]+$/gm, ""); // trim whitespace-only lines - bq = _RunBlockGamut(bq); // recurse - - bq = bq.replace(/(^|\n)/g, "$1 "); - // These leading spaces screw with
     content, so we need to fix that:
    -                    bq = bq.replace(
    -                            /(\s*
    [^\r]+?<\/pre>)/gm,
    -                        function (wholeMatch, m1) {
    -                            var pre = m1;
    -                            // attacklab: hack around Konqueror 3.5.4 bug:
    -                            pre = pre.replace(/^  /mg, "~0");
    -                            pre = pre.replace(/~0/g, "");
    -                            return pre;
    -                        });
    -
    -                    return hashBlock("
    \n" + bq + "\n
    "); - } - ); - return text; - } - - function _FormParagraphs(text, doNotUnhash) { - // - // Params: - // $text - string to process with html

    tags - // - - // Strip leading and trailing lines: - text = text.replace(/^\n+/g, ""); - text = text.replace(/\n+$/g, ""); - - var grafs = text.split(/\n{2,}/g); - var grafsOut = []; - - var markerRe = /~K(\d+)K/; - - // - // Wrap

    tags. - // - var end = grafs.length; - for (var i = 0; i < end; i++) { - var str = grafs[i]; - - // if this is an HTML marker, copy it - if (markerRe.test(str)) { - grafsOut.push(str); - } - else if (/\S/.test(str)) { - str = _RunSpanGamut(str); - str = str.replace(/^([ \t]*)/g, "

    "); - str += "

    " - grafsOut.push(str); - } - - } - // - // Unhashify HTML blocks - // - if (!doNotUnhash) { - end = grafsOut.length; - for (var i = 0; i < end; i++) { - var foundAny = true; - while (foundAny) { // we may need several runs, since the data may be nested - foundAny = false; - grafsOut[i] = grafsOut[i].replace(/~K(\d+)K/g, function (wholeMatch, id) { - foundAny = true; - return g_html_blocks[id]; - }); - } - } - } - return grafsOut.join("\n\n"); - } - - function _EncodeAmpsAndAngles(text) { - // Smart processing for ampersands and angle brackets that need to be encoded. - - // Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: - // http://bumppo.net/projects/amputator/ - text = text.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g, "&"); - - // Encode naked <'s - text = text.replace(/<(?![a-z\/?\$!])/gi, "<"); - - return text; - } - - function _EncodeBackslashEscapes(text) { - // - // Parameter: String. - // Returns: The string, with after processing the following backslash - // escape sequences. - // - - // attacklab: The polite way to do this is with the new - // escapeCharacters() function: - // - // text = escapeCharacters(text,"\\",true); - // text = escapeCharacters(text,"`*_{}[]()>#+-.!",true); - // - // ...but we're sidestepping its use of the (slow) RegExp constructor - // as an optimization for Firefox. This function gets called a LOT. - - text = text.replace(/\\(\\)/g, escapeCharacters_callback); - text = text.replace(/\\([`*_{}\[\]()>#+-.!])/g, escapeCharacters_callback); - return text; - } - - function _DoAutoLinks(text) { - - // note that at this point, all other URL in the text are already hyperlinked as
    - // *except* for the case - - // automatically add < and > around unadorned raw hyperlinks - // must be preceded by space/BOF and followed by non-word/EOF character - text = text.replace(/(^|\s)(https?|ftp)(:\/\/[-A-Z0-9+&@#\/%?=~_|\[\]\(\)!:,\.;]*[-A-Z0-9+&@#\/%=~_|\[\]])($|\W)/gi, "$1<$2$3>$4"); - - // autolink anything like - - var replacer = function (wholematch, m1) { return "" + pluginHooks.plainLinkText(m1) + ""; } - text = text.replace(/<((https?|ftp):[^'">\s]+)>/gi, replacer); - - // Email addresses: - /* - text = text.replace(/ - < - (?:mailto:)? - ( - [-.\w]+ - \@ - [-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+ - ) - > - /gi, _DoAutoLinks_callback()); - */ - - var email_replacer = function(wholematch, m1) { - var mailto = 'mailto:' - var link - var email - if (m1.substring(0, mailto.length) != mailto){ - link = mailto + m1; - email = m1; - } else { - link = m1; - email = m1.substring(mailto.length, m1.length); - } - return "" + pluginHooks.plainLinkText(email) + ""; - } - text = text.replace(/<((?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+))>/gi, email_replacer); - - return text; - } - - function _UnescapeSpecialChars(text) { - // - // Swap back in all the special characters we've hidden. - // - text = text.replace(/~E(\d+)E/g, - function (wholeMatch, m1) { - var charCodeToReplace = parseInt(m1); - return String.fromCharCode(charCodeToReplace); - } - ); - return text; - } - - function _Outdent(text) { - // - // Remove one level of line-leading tabs or spaces - // - - // attacklab: hack around Konqueror 3.5.4 bug: - // "----------bug".replace(/^-/g,"") == "bug" - - text = text.replace(/^(\t|[ ]{1,4})/gm, "~0"); // attacklab: g_tab_width - - // attacklab: clean up hack - text = text.replace(/~0/g, "") - - return text; - } - - function _Detab(text) { - if (!/\t/.test(text)) - return text; - - var spaces = [" ", " ", " ", " "], - skew = 0, - v; - - return text.replace(/[\n\t]/g, function (match, offset) { - if (match === "\n") { - skew = offset + 1; - return match; - } - v = (offset - skew) % 4; - skew = offset + 1; - return spaces[v]; - }); - } - - // - // attacklab: Utility functions - // - - var _problemUrlChars = /(?:["'*()[\]:]|~D)/g; - - // hex-encodes some unusual "problem" chars in URLs to avoid URL detection problems - function encodeProblemUrlChars(url) { - if (!url) - return ""; - - var len = url.length; - - return url.replace(_problemUrlChars, function (match, offset) { - if (match == "~D") // escape for dollar - return "%24"; - if (match == ":") { - if (offset == len - 1 || /[0-9\/]/.test(url.charAt(offset + 1))) - return ":"; - if (url.substring(0, 'mailto:'.length) === 'mailto:') - return ":"; - if (url.substring(0, 'magnet:'.length) === 'magnet:') - return ":"; - } - return "%" + match.charCodeAt(0).toString(16); - }); - } - - - function escapeCharacters(text, charsToEscape, afterBackslash) { - // First we have to escape the escape characters so that - // we can build a character class out of them - var regexString = "([" + charsToEscape.replace(/([\[\]\\])/g, "\\$1") + "])"; - - if (afterBackslash) { - regexString = "\\\\" + regexString; - } - - var regex = new RegExp(regexString, "g"); - text = text.replace(regex, escapeCharacters_callback); - - return text; - } - - - function escapeCharacters_callback(wholeMatch, m1) { - var charCodeToEscape = m1.charCodeAt(0); - return "~E" + charCodeToEscape + "E"; - } - - }; // end of the Markdown.Converter constructor - -})(); diff --git a/static/lib/pagedown/Markdown.Editor.Icons.fw.png b/static/lib/pagedown/Markdown.Editor.Icons.fw.png deleted file mode 100755 index 80b998a44c0be9cc7664165e43961b97f442bb8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51360 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 diff --git a/static/lib/pagedown/Markdown.Editor.Icons.png b/static/lib/pagedown/Markdown.Editor.Icons.png deleted file mode 100755 index 3c9ff7171a0c202a634318e7fc578869278c01f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 52640 zcmd41Q;;W25GUBSZA{y?jcMEZw{6?DZM&y!+wPvWZENS-ecXN6xVwj|h%9DSRYoBy zD}NCRauV<`I50p!K=4wMqDueL&VPgm1@SNLn(Tr8mw-75OQ}Hp3*J!1;s46e_L7=T zKtQl4{}J%Qmhb1kN<3#V4QFLLQ)f3rM-w0+V>=@gB5?~x6E{0YD<>imWmbsB#r%JR ztlK-Psk>)iX-QdYk7E1A6=SYzm4_r!MrOGxc2Y?%2`Bs^)lMYal)@vGO&Wyql_0(e2mwNB=D3v7eN$H0O|%itr#mYhXIpJI*$i4@9||hM3ovB zW)Fh1jQRB9-2C}r`Qg6l?)83~8J(zJ0Nvek+wnPw;SiUQ*s^UcL{9!GKZ~6p z{5B8M$6XzbDU;aTcXR$$--}A6`xzVo?;+WN+EG(S(@$fsz3z9r)V{;SUA^je`$WFs zxI#YQxeLE55is(`j>FPFZXHPLGE&)pTYO}T5X%O2P7I4KMqQoovMW3PL~Q&yOznQp z`b^Y%7vZZ|?uGhNQIPxdgNA1D12HaQDvgCyvf*g{6W8>mo~c~rhkkq$_EkpxO_;)K zOqSE8R*hY;@gwvSVVpL)`90~V^pC@%qPdq-68h_40U}z8qBcdjB<#7D#!K}}E!&&p z8SwZ75hRor{tUFlom04a(cJE-8e>IOSg@m8es0Va?%8dl{v)YZHi9*G2A-qK>zN}* zSX9^8{X1cFL>$kjWvNXK6rx+Gt{lQ9hj<`xBzmowI8^m>QLB=^mbgvv zfV6^KTGedL#4GNrDzHs%{xr&?GprKFxK#+?|4~%UdhI?GLFa%|*`llZErtw@Y^71? z9D$(DI?I7M2`#^r&YI|;RSi{vZya|SCqj+2 zBVq5Rfade5Wg{4|uD8!JVW+j!Ez72dkE(UIDEZJYn1v%RurB5^ZubOE6z7zLK*x=WE`0%HA(Tqt(>{0;zkLt2|N%#vUYLB!7qCC4#gPrJ6 z?Yd#(+2}+3BlE$@Tbz!vDTpf7qyWckYccH)W{8@=21Mr-g&1&RiZ2mmeAW zHzds#8P?0OVeMkzxa;qQv$&RHqqg_HOulOl=nhl`Q-Qb?LK)`4|IQIhx8qO}Z%={wIa8!1|PNH zSv#Wx&K#2cbdbGz2UyITPYQ<DTiQfd1^f(Gg>#(1++D_ zk@DdjNc0vgCt%si5ajIX` z_ERV_IgbZw9%3{El&4*EoNAHxY$0u*mkl!7)InUUbLodS&K#{>(*OmN5ySpLSIo4o zTw{;{-qjGl-#TG%yV*3ZD*CnTf_s~DEoqw&Af@ZKbwguvp{ie0_DTXqMBXHhcJxdG zH;sZvGb7lw$pcHo;ncG1Yq>FJSTV^=+X2cME z?^S4Q^^JYSH0dCy>*cVwi>pIoCHYRB43C`2QUj0DukDR`n&4Red|W4|c!tdmH!s%PXSIryOzBLp>4TG?NTiNQ$F!qc81#T;8U&2BTf$)eUF!<6i+C zjs^}{yTvq|HXG@pz-)>J{zX5<<|M03FoN62bitlb#yiy(^Z0>{RUZ2N8ud({}v?6LrHwMLL{{bBC)^*Em_IbnB?n|(b&0eHwtEg zS-4$2XR98BvC)z9tRu;_$+$AaIh#&@$hl#ULbB9!_f8v~XCz=aA*;!5%6x66{H)3R z9k)tY#Ba>(WwRxJVj9msXx1M?aF1E9RCd|6ei485Z`)k%46fZ6BFyO} z#x16ZL8TrQ^mZVf=tlqOQG{;(zU%75Qchk~@q!kPertN%BpwN5{Cb2jbjk<50DW5H z-w8CCfpt^A{hY+!)QukPazb4AS$wfWTw%sg**`Xj(cw99Gst}#l%69#iNG^TY4EP< zjp*qN<}Xb39B!Mzs>dP9X>!qcOk;xJENbY@W^M&U@cYqeOgmr#bkB z`OP%glm{D*r6O|FKp~y3P2o#ufCl*{eSbN$&&yHsYp? zwYEvN=I(n$0V;oif6;hppJp$-y%}5u-VO*) z`fwf5`{?b|LDN3f;U{}Cn&RU0M_%*n2j9J6J+=XZMx0ly%Z*9xA(w9dF|y0+fgj5r zEp~YON?z9Y{QV*9rt=z6G}p;;R_CDiC*g#EgM4G-rWYlRdr_^Lk`QdkzFypyqxY{a zd7f&GbJ|#4^b!ktQISN@xwu{XsT$_jeMaHG9WkSgkUHV{xLY`VHJgKpM1Y{h^L7<;&;s#BHIDufdgco1m6UJ>lZGn1oO*-}l%41eXD#WQH{ zR2g|6u1%EGFN@(XPG|FhC0-iY=`nhfG00_HH8st{UaS!V`(3<4voiOs9T`44u2YXj z6Ib^|_%V%1e}_9+G^=`2b|eEk)r@nR~r??pWS62|BJ+|%U2W~;LcmlTJn7x9YI z;A@6B^EyoEJ-jh85wxR>J{kkPxxoMKg_Uc6)Xxj2KC;~(-rgIVg{$!EnJ$u(!)7Mv z6i@e2hQ0wG!wdPr{N|_Ui?GbQZgdB;6W~p$8sUw#hGBoxoNnEGuZX9=ks&ZuOSoe8 zcjHIz2W*_bSRVPK1Iq{U&Qx{3C)PD@Fs&9;pzfF z>n^$Di*FzPQ>q2~@^Yg;+vDTtiWlq@eP_C){Q30yZtGLH(qn>ME48!-+Up=n%I}vF z4K^b{z8g(WGT$Joho*XPPz3s_uq@d_?4rxXd7`v&rP<3FDiDL547&`T*oSy7d%qcD zX6b1BA>oq1eMlv@mKi<&&3wG-k$#-x&@(-*p%aL_ORNhC%&=W$8JQQ)GZm(?ZdJ|_xjSl`hFmuF}kAVuaf13tq%Vy z)Wu=mbbqb&EO7P+*zi0ZfRT|f1mR;o9Zv@Igy z_lKyc=}O|5uxh}rQ9G$N_!3IWo7f@dZ&UQxo*VBDPbREf{TK`e2#FJQ>v`NuWA-3F zf;@fjCPm71;|_O^HkRIdjJl=cu%PSDNU=SmpItQm*Qow$dtdbXVFPzpgi#(ZA|p9J zjIY5#dG#Fr&!#4y_r1y|Rja?5e|#-nw2D^3Ry?d4IXU?Qzv2vkc-DFmKNz$YSDIFc z@qAV@=^(a}m|(W}$=5gMOqrrEa<2Q|QgobS^Y2*J6x6u~b-69p2Kk*sKOSqG%Gq}o zO;0pn*RJMY`l)g@FXmde{n0EKMIpbxpv=4HHGE1Mq?vR=5L$RZ z<%!Vwox9NTB#&?J9ABYwCCojinMTlsb%;sPg_Vd&F$cfnq^kyrz0;!&MPYm^~arRwPGhlAm8*wmEVsY zDVorV4WXw7%31+hJq5#*!F5>`$EMlQ6x^%+lIbU1RtmMy3;qN zIdH0-y@v%qV|N(J9?u$Q7kGEVj3p%Fid7BJBIkceE_Pi97f;dkjU_(-$fG$$3tg@U zG)*j}V-e(?FL;bP za%Pzv>OvFK(=^5G)ArSDPxUVned0r!;7=>XXMU5qJn6>O5AMNyMUXgI#y1#p{eD&@ z#uzji60?8LZs(*@ZMg(1yb)*Mz160bgl|?WT@~oz^I(LjJk<>A+iWS{E8-IbJZDZ^ zs`6jxpH=Ol`vYT^m(LY_kK&k_+eq3+4TI$g6RGLI3GCfEL{;~tC4wtlSmPK~YC{qn zk-9GBQ{lWaMAYo8o!{&ZF_P6?Z_89vP)(nOX)MwKbjDwA>s(Oj%4Ap=p{~E8?k| z6NDJZQmcizKwp%COZxW{CVVIt*Gq$Aq8Xr6j!7JT9}OwN^-ddvqm$@dG8J>W6c;rv z^BWz~)~ouX(P+|?nv#=%Q?KUBwW;c7o#P}p{w2dEPp=JRTWAx9S(7UE&oMWkNXV%> z2n%?N$K5?wxbH_xe+SJ;LVze8;?kjCZC5djbZ!OdO;-!tp`Ir@t;=?!D^HQce~}8| z1CtEqvjMk(wg@RiwQ$^rV#IzI#P(HnCnUesQ<4S8Eb=Z#`AL_uX<7@6F-5T06$nEvH@*1m?yww_MplHfMKJY8H!RSq!K;2v+#oMOl2 zut8!uICkqB?+y8xo}11+lQyHYp@L(Qc!Gsx1DQIfFGW#cl}q?Z^??=7bvG_=Steq; z_GHj>w~s~mFil02uM9hDR?E_yFDva$0C6n#k4^<~!S^&(&pHB{26+_)eFYU`W#MC! zVmFsHSE<_8xOh(N>d*p)EW@(W0-!u7j5*g>NKqt?XM1qZi#NkO7}_T~Z06pRrzWV4 zwrvz6$LY9M!U*Duu1#S>v)o*$L$2j0!>nc_$6OXR=p&ABOTFnx#65UFauTWnHAr`O zY=OjYinhZFrKFfOk(VLI_8sHzSQW`_K0FW*0PO?X`a^VVlnGe!v*n%7U3X4 z$niKsuvwnF<&=+T80Ipzvx{uNY*fzUp6+SmvjXfb6OCOue(40QXpx^i)9XE*X1=;& zS2fV)vC2E5TP@&0r`{?N2EXU;UznF3SbffMU?Iq!b1>pnL=r4BvR#09r4O_r;ZN!~ z+Emxcxppt;qlVTFLtTH$1E_V<<$k#hpj@15Rb#pg1yrIbY8f4m@mORU9Z%JqyEBDb zuz&3U9fX5SEFYiM;eIULQc|BxE^S*@4+MVBS>(3$mc6_zeS=BguN1hh6f`P;QemI= zNW)*iKdT3ySl%SQ%%!ZruSAeFo5bcM6{wM4J<^MXL=>VkzoDmZpiu$n-7yZ~@;xPD z-$}rC$!ULtgDADO*iu_VRC9|e5*Na~!;>J9WQG+Z%hlKq5{fXo>b#xtvk6K$g$UXt z3}@`Bke#^GE#nBV$28ifWwQ}Nr*(|SY|$G>rNACSF{A+NhAn>{w?pYP+-3q)^T1^s z(UF(*(ab^m8>{Vhf@eM^3!Jc>cgXo)==h!#-7@?6MBIHAi=GZMJVFy#GTBGeOHi!5 zjLPmZ!cF64FlZqeRUvPoFMEpv)z=TovatGmpFpZZ40Wzqr~dspWpB^jvayF-JSG4WGe!EuM0dzl zEGc9URl%yhtG5a)qLnXM%_Z#9X%9p#{53@COeHypaJSU4GdMJznly1P6Mbz!POVjwo5W?K z6KK{+>99ewdr3w0ggnb6zJ*t`j7&}?&xQp@QFu;6cJxSHnC&97c+GEm0rwH!Q0_tA zdep_$8Jtz9SGzx&C2$Q*5r2K7^FG7Gd90=luAmtr?B(gX_Al!QC_BhCZ^bS7Y1@Iu zY&TMw#8sDZ6g-mSbT7|uD%Z3E6R`wr8*Lpnmvom!#Oj=6t8$w0#l$9yOT>s@m_Hga zqd3q50Wz%hSZ1eC(*b5Qu{C8}6|5Av(={v@DG>Bj-|dXD5QpZ=gz*@(rooSR=ntgO`fmQB?S*{$y$My$kLL^mz4tSp*T zJ7aTnR2NM`YNg?4%6!GkG|VJ%aO;p3&1`q(=8{XxCyC3uJpM@`b>mc90$p$w&LwC- zy?-3@PHnv@MgVb`@A~vh3JSjqv-bIeK@CGwYrW+oYKwUAcH%XN~zJwcC|Z zx-_g6fjo1YzQ?dH>S`GZIw7*^_DEP&?^Z@PK+wbP<)$Cf4Ou;m;h7w88XtTro>54c zIU?*hN`N{sVoj41OcKI<8TC)uCh&=WyHD@*&FM^1_N<5pEXv(jtdE;qhzg86cVzdv zU&eg4`MU|1+13)D)C%Fu4bqLf6A%2PgG#k&mEy#=A_~~xe(SGUr$M$=Wc@|&iev3+ zGZjXkyQ7=xO`cDv8s+su>P*3%u?Tt_m|^A2Pb6gbL<{Wk-xQdbT@}EU6ycxpiD;o8Iu07l> zwvQjG$llKG>$0jXJySg=aYp>PLQ`65*WDfYM{#Tx2KL{%v)Ik_n4MpHWmVTR^xqGB zDQd3TPM+qt58t3;Y)c@gPG9Fc*Z;oUFX}qTf%X)SHzeAv;!fEBkTtbE$3K5+wTSWI zwcx%z>9#!)@+j&d1@?NVx-ZBAeTebyPV3~<?#@NM3CcIxo~VMprB@++2^9yF^hXbt!{^bAIoX2Ekb+q zW#vz8*y}15%zVYJ@O58^+l3En-DT{wKY>DZoqK%39%W;6zurrcYmk^Yzj!PLN2t50 zEewyn4WGPWwH{KX!JAO%ymPo7!ZA$r&N0dJl$imwMk}i4cV*eKWrOMJiMQCNv|KM3?69DS}Z=d_|p1ZbIr!~VER z9hga`x@nqY?_`!KKV@KsU|8w5r|4M3S$$0Xz)F6?UEL&Lm?o3!nA>Fc@ofiQRjTKG zfv~^jihqJ%PBL#|XZuwXz%9FZBpd!U!=53FtnHuC&dh=I$Js_4)ft?F7ujJIfrJ;~ zfZJkpj(PPEtg_bUxcpoH5YyDOQ|~l51JAP>o)<=>-Rqlgcp6$@mt9~}YlGGCUtSmP zOM&Gvj`i6$*sII-F0=?Yw#Okh6<&e^Z_C~}hLU@5{=bfW4&dl;LT&$(fr0yKJ>m}I z&%*#~U5dM8&)%|smUUvcTiM zr1sWscq1B|Z*pBh*Xlz{Tl*bkBG1w6YrWde_{_^`g~CdEr?+`Pp#F+)xh3t1$JP5? zb+S4U3mGKugPMJ_B@&?c@mIi>)biz^mSo;E#Q`wWA$amoiQN!x- zFRH>06u-bxyXSndKHV~T>v>*ux}Q586?vuE^9E>x@W+gIltOokqJd70!z5VMrX77C zlv*}^nuI!PPI9-rFj7q8b?SB=7%L;M;Y4#a{&RU5*F9a;N>5>Gyi>=GUb0E5l4Shy z^JVc>+73kTxFWX;h%-!mfD?M+bZd8^&mfae> z@>23{zBs_U{k9ea2=&vEF^LWI&n$P8Az@71J~tvnQ#$!33L3l#J97`jjsGh3N}k&0 z!~ix1K3!^``5f{a-fiV`G?I7D3=yC|2rl*NA6jdC-r2@}g0WDnmNC^MN;D46Z!#d`*b;3D!8;f4yz~E^z3WI&ismm z8~~f9p^c&LCiZ_?uZZ;|)%)#mmtoxu8r32CUq8tU=yvn|?I%3-E@6*yCVcT}#uT7U zHQtY&CpY1j^YAHhB*aerTfWrUcr9W7NwVCPX95_olIW$4@IC5|@{M}`E7~Uu_CF)< zpI^ISX9S#;YqwW)tMz4QA(E8}^5u?vg((OPs)HltrpemyLM)SiY8_o^KCi`TymlC1 z%$juBE+OYq^)1M8w4c3`h&;e9`Hg;(u`&ln zfp3q)MKVE4QJ=yZloJ!-oq1N#5_oFLs<{2JIX9u7q$yjH^!UMD#Hy$Oe+`BETY1rwA_b7knDyccX{g{roZ6blmG950dUuEaIrqM4Te3 zld!KNO`<~;@fvO+emZFu-?ACYK4fhB#YceQa zl4(6owc%BKw@-pN9(BJPBQW2^jUZQ_VwerC=}0>xG}9&rhVhZ%?nPmbntczxlMDg8h_cY|* z_dgQ|>KuS=%z5l0?Z{d6m(OF57j2`}KPx#$x?XI$)(zKwXnNXR6SdK9gsr~Tdo1_8 z)_pKWUtqZBe3F(E*=v}2wzx^?`8i<8lQH8U$H`j`6MKx$UGOxN*H6>heH`|&9d5dH zA2kmXl>{#VdJV2ZG!r{yvs=e6E35CpoOn*WIu@nO{7)(ajw6`cRGXVV-SEdQMc+V=!>llaWOi07FI(u}!`(eRvfl7l-V*Y8-x@7PSawDbuezcKSICxT+;Q4ZMVG|*~`})f0 z{6ySjne88)AmGy5w!JPS z47uPpn9L>>rwpr)upUrTuMMQFF1=@)<`>s`$~vQS&s4*x%0GA+V7h&<{-ot~`~HYm zS^KWGROhCCJU+IU+j2iG; zc9ZgU&<6Oa_SzK*D@sV{Yd@HmElG48=$=W{Aa&(*oSpaVaOm17s(2UNC-W>JUU%j+ zqfdTy!{>NQ(5%d8TiODk_;^_kZ7pgyo`a)+V*R*$mDpS@6-_L7$REGpeR4^3U8##C9yr5?m()j;lg&qv>9 zaG5-FR&f;3M4L*Wbq2Gx@O~jc)RN?2#qQ#LUh(efLk?SeeM>i7lPa^#U7%w~h$!ss zOT4%C`n;c2HaWTbvgLU_n$0WkfPHB`zDM-*7J9ViEx3_b$oeI$JGJ~Vo8!aQ4_I29 z;j(r0`h!i7qwnzQ>ghX-(JhoFhYh#X;M438fm`2}_9d_0U8LM)a1t+fMmW%lgo9|a zIIZe;Il{4%X@~m`A?^l8&KW?rw#%yZC?A(X7wFS8?E0f%6KX!Oxykcr)A>7#IQ;@{ z<}z0w+_52S<$~^e{^#T^C2q6e-LsJ02d#dUr*B#y)O`l3rStrPYT{$GUgY4YULN!a z3-W&Z7U=~aVb*?8HEWO{G<3oHVe#{ZlMkKj*(>8ByQcOh-{)E8$&dEVmwz62H`hoYT-lmTVy|tuzn=mL4)YXpIf01u0pVL`@{VOO4~GZ zhkMU728TB<_M_|m(dP<~nF4}IBceYfFmenSVPRnN^YJr08^laoph~wh#LO@Bk$>L^ zUTz5XyD_3LA?Hpo&x?nR&U|xw2vcj8FE;Opnc4Vwr?=*-oJWI-x{XIClL+_(tJoh; zX_Eqcon7xl2r2!#)<$DAvJw1vdm{yZAc<}tfn*r6<$3);^sb$8d6ZNcAE+e>Vkm@M z#+9THQg+)>rm_P^3m&Oqk7>h(MWG2gwecF;RgbuWPWRxv88nQQj>fO8a%na%olJgx zc`47PsIuZQ*l`vxXD2M$4`cw%Q{(^^t_mt0)w>iab%T~Y^3G_HiUvL|;orx7o*@pA zBIw@BKE}Ug!OFDjcXGXXX?Pv444PR^58A(!U9QKea(Or*c!PKO6=uhOgWWtHq6=^A zoDthSht(_JyPgh8aQ5xDM1w``UeMz~8?7C?y)3|bN-IIepSl4p1`&r4!&p+-6NoO>?8_&Z35?h{c4L8U20$ZD*zFdA7!HL$ppD3Z9H@p?y%!b8fzRmDmf;K zfr;YY?&)Pj2cZ3Y&oPwI=R+0>B!BVHSqTIgmJ>VeLZaUiO6o^0mJPKAx31BrY!tu} zhQ(xXoo}hvjJn2#4_XSBiWd^b5qsyg7V9EuV}_l=G@Y6b)zL zd}YYw*IcYx6i^XWLcoejb!H`uIH02^xjF+2veipirmfqpIM{DS≀I^VWo?!B|8c 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"; - var imageDialogText = "

    http://example.com/images/diagram.jpg \"optional title\"

    "; - - // The default text that appears in the dialog input box when entering - // links. - var imageDefaultText = "http://"; - var linkDefaultText = "http://"; - - var defaultHelpHoverTitle = "Markdown Editing Help"; - - // ------------------------------------------------------------------- - // END OF YOUR CHANGES - // ------------------------------------------------------------------- - - // help, if given, should have a property "handler", the click handler for the help button, - // and can have an optional property "title" for the button's tooltip (defaults to "Markdown Editing Help"). - // If help isn't given, not help button is created. - // - // The constructed editor object has the methods: - // - getConverter() returns the markdown converter object that was passed to the constructor - // - run() actually starts the editor; should be called after all necessary plugins are registered. Calling this more than once is a no-op. - // - refreshPreview() forces the preview to be updated. This method is only available after run() was called. - Markdown.Editor = function (markdownConverter, idPostfix, help) { - - idPostfix = idPostfix || ""; - - var hooks = this.hooks = new Markdown.HookCollection(); - hooks.addNoop("onPreviewRefresh"); // called with no arguments after the preview has been refreshed - hooks.addNoop("postBlockquoteCreation"); // called with the user's selection *after* the blockquote was created; should return the actual to-be-inserted text - hooks.addFalse("insertImageDialog"); /* called with one parameter: a callback to be called with the URL of the image. If the application creates - * its own image insertion dialog, this hook should return true, and the callback should be called with the chosen - * image url (or null if the user cancelled). If this hook returns false, the default dialog will be used. - */ - - this.getConverter = function () { return markdownConverter; } - - var that = this, - panels; - - this.run = function () { - if (panels) - return; // already initialized - - panels = new PanelCollection(idPostfix); - var commandManager = new CommandManager(hooks); - var previewManager = new PreviewManager(markdownConverter, panels, function () { hooks.onPreviewRefresh(); }); - var undoManager, uiManager; - - if (!/\?noundo/.test(doc.location.href)) { - undoManager = new UndoManager(function () { - previewManager.refresh(); - if (uiManager) // not available on the first call - uiManager.setUndoRedoButtonStates(); - }, panels); - this.textOperation = function (f) { - undoManager.setCommandMode(); - f(); - that.refreshPreview(); - } - } - - uiManager = new UIManager(idPostfix, panels, undoManager, previewManager, commandManager, help); - uiManager.setUndoRedoButtonStates(); - - var forceRefresh = that.refreshPreview = function () { previewManager.refresh(true); }; - - forceRefresh(); - }; - - } - - // before: contains all the text in the input box BEFORE the selection. - // after: contains all the text in the input box AFTER the selection. - function Chunks() { } - - // startRegex: a regular expression to find the start tag - // endRegex: a regular expresssion to find the end tag - Chunks.prototype.findTags = function (startRegex, endRegex) { - - var chunkObj = this; - var regex; - - if (startRegex) { - - regex = util.extendRegExp(startRegex, "", "$"); - - this.before = this.before.replace(regex, - function (match) { - chunkObj.startTag = chunkObj.startTag + match; - return ""; - }); - - regex = util.extendRegExp(startRegex, "^", ""); - - this.selection = this.selection.replace(regex, - function (match) { - chunkObj.startTag = chunkObj.startTag + match; - return ""; - }); - } - - if (endRegex) { - - regex = util.extendRegExp(endRegex, "", "$"); - - this.selection = this.selection.replace(regex, - function (match) { - chunkObj.endTag = match + chunkObj.endTag; - return ""; - }); - - regex = util.extendRegExp(endRegex, "^", ""); - - this.after = this.after.replace(regex, - function (match) { - chunkObj.endTag = match + chunkObj.endTag; - return ""; - }); - } - }; - - // If remove is false, the whitespace is transferred - // to the before/after regions. - // - // If remove is true, the whitespace disappears. - Chunks.prototype.trimWhitespace = function (remove) { - var beforeReplacer, afterReplacer, that = this; - if (remove) { - beforeReplacer = afterReplacer = ""; - } else { - beforeReplacer = function (s) { that.before += s; return ""; } - afterReplacer = function (s) { that.after = s + that.after; return ""; } - } - - this.selection = this.selection.replace(/^(\s*)/, beforeReplacer).replace(/(\s*)$/, afterReplacer); - }; - - - Chunks.prototype.skipLines = function (nLinesBefore, nLinesAfter, findExtraNewlines) { - - if (nLinesBefore === undefined) { - nLinesBefore = 1; - } - - if (nLinesAfter === undefined) { - nLinesAfter = 1; - } - - nLinesBefore++; - nLinesAfter++; - - var regexText; - var replacementText; - - // chrome bug ... documented at: http://meta.stackoverflow.com/questions/63307/blockquote-glitch-in-editor-in-chrome-6-and-7/65985#65985 - if (navigator.userAgent.match(/Chrome/)) { - "X".match(/()./); - } - - this.selection = this.selection.replace(/(^\n*)/, ""); - - this.startTag = this.startTag + re.$1; - - this.selection = this.selection.replace(/(\n*$)/, ""); - this.endTag = this.endTag + re.$1; - this.startTag = this.startTag.replace(/(^\n*)/, ""); - this.before = this.before + re.$1; - this.endTag = this.endTag.replace(/(\n*$)/, ""); - this.after = this.after + re.$1; - - if (this.before) { - - regexText = replacementText = ""; - - while (nLinesBefore--) { - regexText += "\\n?"; - replacementText += "\n"; - } - - if (findExtraNewlines) { - regexText = "\\n*"; - } - this.before = this.before.replace(new re(regexText + "$", ""), replacementText); - } - - if (this.after) { - - regexText = replacementText = ""; - - while (nLinesAfter--) { - regexText += "\\n?"; - replacementText += "\n"; - } - if (findExtraNewlines) { - regexText = "\\n*"; - } - - this.after = this.after.replace(new re(regexText, ""), replacementText); - } - }; - - // end of Chunks - - // A collection of the important regions on the page. - // Cached so we don't have to keep traversing the DOM. - // Also holds ieCachedRange and ieCachedScrollTop, where necessary; working around - // this issue: - // Internet explorer has problems with CSS sprite buttons that use HTML - // lists. When you click on the background image "button", IE will - // select the non-existent link text and discard the selection in the - // textarea. The solution to this is to cache the textarea selection - // on the button's mousedown event and set a flag. In the part of the - // code where we need to grab the selection, we check for the flag - // and, if it's set, use the cached area instead of querying the - // textarea. - // - // This ONLY affects Internet Explorer (tested on versions 6, 7 - // and 8) and ONLY on button clicks. Keyboard shortcuts work - // normally since the focus never leaves the textarea. - function PanelCollection(postfix) { - this.buttonBar = doc.getElementById("wmd-button-bar" + postfix); - this.preview = doc.getElementById("wmd-preview" + postfix); - this.input = doc.getElementById("wmd-input" + postfix); - }; - - // Returns true if the DOM element is visible, false if it's hidden. - // Checks if display is anything other than none. - util.isVisible = function (elem) { - - if (window.getComputedStyle) { - // Most browsers - return window.getComputedStyle(elem, null).getPropertyValue("display") !== "none"; - } - else if (elem.currentStyle) { - // IE - return elem.currentStyle["display"] !== "none"; - } - }; - - - // Adds a listener callback to a DOM element which is fired on a specified - // event. - util.addEvent = function (elem, event, listener) { - if (elem.attachEvent) { - // IE only. The "on" is mandatory. - elem.attachEvent("on" + event, listener); - } - else { - // Other browsers. - elem.addEventListener(event, listener, false); - } - }; - - - // Removes a listener callback from a DOM element which is fired on a specified - // event. - util.removeEvent = function (elem, event, listener) { - if (elem.detachEvent) { - // IE only. The "on" is mandatory. - elem.detachEvent("on" + event, listener); - } - else { - // Other browsers. - elem.removeEventListener(event, listener, false); - } - }; - - // Converts \r\n and \r to \n. - util.fixEolChars = function (text) { - text = text.replace(/\r\n/g, "\n"); - text = text.replace(/\r/g, "\n"); - return text; - }; - - // Extends a regular expression. Returns a new RegExp - // using pre + regex + post as the expression. - // Used in a few functions where we have a base - // expression and we want to pre- or append some - // conditions to it (e.g. adding "$" to the end). - // The flags are unchanged. - // - // regex is a RegExp, pre and post are strings. - util.extendRegExp = function (regex, pre, post) { - - if (pre === null || pre === undefined) { - pre = ""; - } - if (post === null || post === undefined) { - post = ""; - } - - var pattern = regex.toString(); - var flags; - - // Replace the flags with empty space and store them. - pattern = pattern.replace(/\/([gim]*)$/, function (wholeMatch, flagsPart) { - flags = flagsPart; - return ""; - }); - - // Remove the slash delimiters on the regular expression. - pattern = pattern.replace(/(^\/|\/$)/g, ""); - pattern = pre + pattern + post; - - return new re(pattern, flags); - } - - // UNFINISHED - // The assignment in the while loop makes jslint cranky. - // I'll change it to a better loop later. - position.getTop = function (elem, isInner) { - var result = elem.offsetTop; - if (!isInner) { - while (elem = elem.offsetParent) { - result += elem.offsetTop; - } - } - return result; - }; - - position.getHeight = function (elem) { - return elem.offsetHeight || elem.scrollHeight; - }; - - position.getWidth = function (elem) { - return elem.offsetWidth || elem.scrollWidth; - }; - - position.getPageSize = function () { - - var scrollWidth, scrollHeight; - var innerWidth, innerHeight; - - // It's not very clear which blocks work with which browsers. - if (self.innerHeight && self.scrollMaxY) { - scrollWidth = doc.body.scrollWidth; - scrollHeight = self.innerHeight + self.scrollMaxY; - } - else if (doc.body.scrollHeight > doc.body.offsetHeight) { - scrollWidth = doc.body.scrollWidth; - scrollHeight = doc.body.scrollHeight; - } - else { - scrollWidth = doc.body.offsetWidth; - scrollHeight = doc.body.offsetHeight; - } - - if (self.innerHeight) { - // Non-IE browser - innerWidth = self.innerWidth; - innerHeight = self.innerHeight; - } - else if (doc.documentElement && doc.documentElement.clientHeight) { - // Some versions of IE (IE 6 w/ a DOCTYPE declaration) - innerWidth = doc.documentElement.clientWidth; - innerHeight = doc.documentElement.clientHeight; - } - else if (doc.body) { - // Other versions of IE - innerWidth = doc.body.clientWidth; - innerHeight = doc.body.clientHeight; - } - - var maxWidth = Math.max(scrollWidth, innerWidth); - var maxHeight = Math.max(scrollHeight, innerHeight); - return [maxWidth, maxHeight, innerWidth, innerHeight]; - }; - - // Handles pushing and popping TextareaStates for undo/redo commands. - // I should rename the stack variables to list. - function UndoManager(callback, panels) { - - var undoObj = this; - var undoStack = []; // A stack of undo states - var stackPtr = 0; // The index of the current state - var mode = "none"; - var lastState; // The last state - var timer; // The setTimeout handle for cancelling the timer - var inputStateObj; - - // Set the mode for later logic steps. - var setMode = function (newMode, noSave) { - if (mode != newMode) { - mode = newMode; - if (!noSave) { - saveState(); - } - } - - if (!uaSniffed.isIE || mode != "moving") { - timer = setTimeout(refreshState, 1); - } - else { - inputStateObj = null; - } - }; - - var refreshState = function (isInitialState) { - inputStateObj = new TextareaState(panels, isInitialState); - timer = undefined; - }; - - this.setCommandMode = function () { - mode = "command"; - saveState(); - timer = setTimeout(refreshState, 0); - }; - - this.canUndo = function () { - return stackPtr > 1; - }; - - this.canRedo = function () { - if (undoStack[stackPtr + 1]) { - return true; - } - return false; - }; - - // Removes the last state and restores it. - this.undo = function () { - - if (undoObj.canUndo()) { - if (lastState) { - // What about setting state -1 to null or checking for undefined? - lastState.restore(); - lastState = null; - } - else { - undoStack[stackPtr] = new TextareaState(panels); - undoStack[--stackPtr].restore(); - - if (callback) { - callback(); - } - } - } - - mode = "none"; - panels.input.focus(); - refreshState(); - }; - - // Redo an action. - this.redo = function () { - - if (undoObj.canRedo()) { - - undoStack[++stackPtr].restore(); - - if (callback) { - callback(); - } - } - - mode = "none"; - panels.input.focus(); - refreshState(); - }; - - // Push the input area state to the stack. - var saveState = function () { - var currState = inputStateObj || new TextareaState(panels); - - if (!currState) { - return false; - } - if (mode == "moving") { - if (!lastState) { - lastState = currState; - } - return; - } - if (lastState) { - if (undoStack[stackPtr - 1].text != lastState.text) { - undoStack[stackPtr++] = lastState; - } - lastState = null; - } - undoStack[stackPtr++] = currState; - undoStack[stackPtr + 1] = null; - if (callback) { - callback(); - } - }; - - var handleCtrlYZ = function (event) { - - var handled = false; - - if (event.ctrlKey || event.metaKey) { - - // IE and Opera do not support charCode. - var keyCode = event.charCode || event.keyCode; - var keyCodeChar = String.fromCharCode(keyCode); - - switch (keyCodeChar) { - - case "y": - undoObj.redo(); - handled = true; - break; - - case "z": - if (!event.shiftKey) { - undoObj.undo(); - } - else { - undoObj.redo(); - } - handled = true; - break; - } - } - - if (handled) { - if (event.preventDefault) { - event.preventDefault(); - } - if (window.event) { - window.event.returnValue = false; - } - return; - } - }; - - // Set the mode depending on what is going on in the input area. - var handleModeChange = function (event) { - - if (!event.ctrlKey && !event.metaKey) { - - var keyCode = event.keyCode; - - if ((keyCode >= 33 && keyCode <= 40) || (keyCode >= 63232 && keyCode <= 63235)) { - // 33 - 40: page up/dn and arrow keys - // 63232 - 63235: page up/dn and arrow keys on safari - setMode("moving"); - } - else if (keyCode == 8 || keyCode == 46 || keyCode == 127) { - // 8: backspace - // 46: delete - // 127: delete - setMode("deleting"); - } - else if (keyCode == 13) { - // 13: Enter - setMode("newlines"); - } - else if (keyCode == 27) { - // 27: escape - setMode("escape"); - } - else if ((keyCode < 16 || keyCode > 20) && keyCode != 91) { - // 16-20 are shift, etc. - // 91: left window key - // I think this might be a little messed up since there are - // a lot of nonprinting keys above 20. - setMode("typing"); - } - } - }; - - var setEventHandlers = function () { - util.addEvent(panels.input, "keypress", function (event) { - // keyCode 89: y - // keyCode 90: z - if ((event.ctrlKey || event.metaKey) && (event.keyCode == 89 || event.keyCode == 90)) { - event.preventDefault(); - } - }); - - var handlePaste = function () { - if (uaSniffed.isIE || (inputStateObj && inputStateObj.text != panels.input.value)) { - if (timer == undefined) { - mode = "paste"; - saveState(); - refreshState(); - } - } - }; - - util.addEvent(panels.input, "keydown", handleCtrlYZ); - util.addEvent(panels.input, "keydown", handleModeChange); - util.addEvent(panels.input, "mousedown", function () { - setMode("moving"); - }); - - panels.input.onpaste = handlePaste; - panels.input.ondrop = handlePaste; - }; - - var init = function () { - setEventHandlers(); - refreshState(true); - saveState(); - }; - - init(); - } - - // end of UndoManager - - // The input textarea state/contents. - // This is used to implement undo/redo by the undo manager. - function TextareaState(panels, isInitialState) { - - // Aliases - var stateObj = this; - var inputArea = panels.input; - this.init = function () { - if (!util.isVisible(inputArea)) { - return; - } - if (!isInitialState && doc.activeElement && doc.activeElement !== inputArea) { // this happens when tabbing out of the input box - return; - } - - this.setInputAreaSelectionStartEnd(); - this.scrollTop = inputArea.scrollTop; - if (!this.text && inputArea.selectionStart || inputArea.selectionStart === 0) { - this.text = inputArea.value; - } - - } - - // Sets the selected text in the input box after we've performed an - // operation. - this.setInputAreaSelection = function () { - - if (!util.isVisible(inputArea)) { - return; - } - - if (inputArea.selectionStart !== undefined && !uaSniffed.isOpera) { - - inputArea.focus(); - inputArea.selectionStart = stateObj.start; - inputArea.selectionEnd = stateObj.end; - inputArea.scrollTop = stateObj.scrollTop; - } - else if (doc.selection) { - - if (doc.activeElement && doc.activeElement !== inputArea) { - return; - } - - inputArea.focus(); - var range = inputArea.createTextRange(); - range.moveStart("character", -inputArea.value.length); - range.moveEnd("character", -inputArea.value.length); - range.moveEnd("character", stateObj.end); - range.moveStart("character", stateObj.start); - range.select(); - } - }; - - this.setInputAreaSelectionStartEnd = function () { - - if (!panels.ieCachedRange && (inputArea.selectionStart || inputArea.selectionStart === 0)) { - - stateObj.start = inputArea.selectionStart; - stateObj.end = inputArea.selectionEnd; - } - else if (doc.selection) { - - stateObj.text = util.fixEolChars(inputArea.value); - - // IE loses the selection in the textarea when buttons are - // clicked. On IE we cache the selection. Here, if something is cached, - // we take it. - var range = panels.ieCachedRange || doc.selection.createRange(); - - var fixedRange = util.fixEolChars(range.text); - var marker = "\x07"; - var markedRange = marker + fixedRange + marker; - range.text = markedRange; - var inputText = util.fixEolChars(inputArea.value); - - range.moveStart("character", -markedRange.length); - range.text = fixedRange; - - stateObj.start = inputText.indexOf(marker); - stateObj.end = inputText.lastIndexOf(marker) - marker.length; - - var len = stateObj.text.length - util.fixEolChars(inputArea.value).length; - - if (len) { - range.moveStart("character", -fixedRange.length); - while (len--) { - fixedRange += "\n"; - stateObj.end += 1; - } - range.text = fixedRange; - } - - if (panels.ieCachedRange) - stateObj.scrollTop = panels.ieCachedScrollTop; // this is set alongside with ieCachedRange - - panels.ieCachedRange = null; - - this.setInputAreaSelection(); - } - }; - - // Restore this state into the input area. - this.restore = function () { - - if (stateObj.text != undefined && stateObj.text != inputArea.value) { - inputArea.value = stateObj.text; - } - this.setInputAreaSelection(); - inputArea.scrollTop = stateObj.scrollTop; - }; - - // Gets a collection of HTML chunks from the inptut textarea. - this.getChunks = function () { - - var chunk = new Chunks(); - chunk.before = util.fixEolChars(stateObj.text.substring(0, stateObj.start)); - chunk.startTag = ""; - chunk.selection = util.fixEolChars(stateObj.text.substring(stateObj.start, stateObj.end)); - chunk.endTag = ""; - chunk.after = util.fixEolChars(stateObj.text.substring(stateObj.end)); - chunk.scrollTop = stateObj.scrollTop; - - return chunk; - }; - - // Sets the TextareaState properties given a chunk of markdown. - this.setChunks = function (chunk) { - - chunk.before = chunk.before + chunk.startTag; - chunk.after = chunk.endTag + chunk.after; - - this.start = chunk.before.length; - this.end = chunk.before.length + chunk.selection.length; - this.text = chunk.before + chunk.selection + chunk.after; - this.scrollTop = chunk.scrollTop; - }; - this.init(); - }; - - function PreviewManager(converter, panels, previewRefreshCallback) { - - var managerObj = this; - var timeout; - var elapsedTime; - var oldInputText; - var maxDelay = 3000; - var startType = "delayed"; // The other legal value is "manual" - - // Adds event listeners to elements - var setupEvents = function (inputElem, listener) { - - util.addEvent(inputElem, "input", listener); - inputElem.onpaste = listener; - inputElem.ondrop = listener; - - util.addEvent(inputElem, "keypress", listener); - util.addEvent(inputElem, "keydown", listener); - }; - - var getDocScrollTop = function () { - - var result = 0; - - if (window.innerHeight) { - result = window.pageYOffset; - } - else - if (doc.documentElement && doc.documentElement.scrollTop) { - result = doc.documentElement.scrollTop; - } - else - if (doc.body) { - result = doc.body.scrollTop; - } - - return result; - }; - - var makePreviewHtml = function () { - - // If there is no registered preview panel - // there is nothing to do. - if (!panels.preview) - return; - - - var text = panels.input.value; - if (text && text == oldInputText) { - return; // Input text hasn't changed. - } - else { - oldInputText = text; - } - - var prevTime = new Date().getTime(); - - text = converter.makeHtml(text); - - // Calculate the processing time of the HTML creation. - // It's used as the delay time in the event listener. - var currTime = new Date().getTime(); - elapsedTime = currTime - prevTime; - - pushPreviewHtml(text); - }; - - // setTimeout is already used. Used as an event listener. - var applyTimeout = function () { - - if (timeout) { - clearTimeout(timeout); - timeout = undefined; - } - - if (startType !== "manual") { - - var delay = 0; - - if (startType === "delayed") { - delay = elapsedTime; - } - - if (delay > maxDelay) { - delay = maxDelay; - } - timeout = setTimeout(makePreviewHtml, delay); - } - }; - - var getScaleFactor = function (panel) { - if (panel.scrollHeight <= panel.clientHeight) { - return 1; - } - return panel.scrollTop / (panel.scrollHeight - panel.clientHeight); - }; - - var setPanelScrollTops = function () { - if (panels.preview) { - panels.preview.scrollTop = (panels.preview.scrollHeight - panels.preview.clientHeight) * getScaleFactor(panels.preview); - } - }; - - this.refresh = function (requiresRefresh) { - - if (requiresRefresh) { - oldInputText = ""; - makePreviewHtml(); - } - else { - applyTimeout(); - } - }; - - this.processingTime = function () { - return elapsedTime; - }; - - var isFirstTimeFilled = true; - - // IE doesn't let you use innerHTML if the element is contained somewhere in a table - // (which is the case for inline editing) -- in that case, detach the element, set the - // value, and reattach. Yes, that *is* ridiculous. - var ieSafePreviewSet = function (text) { - var preview = panels.preview; - var parent = preview.parentNode; - var sibling = preview.nextSibling; - parent.removeChild(preview); - preview.innerHTML = text; - if (!sibling) - parent.appendChild(preview); - else - parent.insertBefore(preview, sibling); - } - - var nonSuckyBrowserPreviewSet = function (text) { - panels.preview.innerHTML = text; - } - - var previewSetter; - - var previewSet = function (text) { - if (previewSetter) - return previewSetter(text); - - try { - nonSuckyBrowserPreviewSet(text); - previewSetter = nonSuckyBrowserPreviewSet; - } catch (e) { - previewSetter = ieSafePreviewSet; - previewSetter(text); - } - }; - - var pushPreviewHtml = function (text) { - - var emptyTop = position.getTop(panels.input) - getDocScrollTop(); - - if (panels.preview) { - previewSet(text); - previewRefreshCallback(); - } - - setPanelScrollTops(); - - if (isFirstTimeFilled) { - isFirstTimeFilled = false; - return; - } - - var fullTop = position.getTop(panels.input) - getDocScrollTop(); - - if (uaSniffed.isIE) { - setTimeout(function () { - window.scrollBy(0, fullTop - emptyTop); - }, 0); - } - else { - window.scrollBy(0, fullTop - emptyTop); - } - }; - - var init = function () { - - setupEvents(panels.input, applyTimeout); - makePreviewHtml(); - - if (panels.preview) { - panels.preview.scrollTop = 0; - } - }; - - init(); - }; - - - // This simulates a modal dialog box and asks for the URL when you - // click the hyperlink or image buttons. - // - // text: The html for the input box. - // defaultInputText: The default value that appears in the input box. - // callback: The function which is executed when the prompt is dismissed, either via OK or Cancel. - // It receives a single argument; either the entered text (if OK was chosen) or null (if Cancel - // was chosen). - ui.prompt = function (title, text, defaultInputText, callback) { - - // These variables need to be declared at this level since they are used - // in multiple functions. - var dialog; // The dialog box. - var input; // The text box where you enter the hyperlink. - - - if (defaultInputText === undefined) { - defaultInputText = ""; - } - - // Used as a keydown event handler. Esc dismisses the prompt. - // Key code 27 is ESC. - var checkEscape = function (key) { - var code = (key.charCode || key.keyCode); - if (code === 27) { - close(true); - } - }; - - // Dismisses the hyperlink input box. - // isCancel is true if we don't care about the input text. - // isCancel is false if we are going to keep the text. - var close = function (isCancel) { - util.removeEvent(doc.body, "keydown", checkEscape); - var text = input.value; - - if (isCancel) { - text = null; - } - else { - // Fixes common pasting errors. - text = text.replace(/^http:\/\/(https?|ftp):\/\//, '$1://'); - if (!/^(?:https?|ftp):\/\//.test(text)) - text = 'http://' + text; - } - - $(dialog).modal('hide'); - - callback(text); - return false; - }; - - - - // Create the text input box form/window. - var createDialog = function () { - // - - // The main dialog box. - dialog = doc.createElement("div"); - dialog.className = "modal hide fade"; - dialog.style.display = "none"; - - // The header. - var header = doc.createElement("div"); - header.className = "modal-header"; - header.innerHTML = '×

    '+title+'

    '; - dialog.appendChild(header); - - // The body. - var body = doc.createElement("div"); - body.className = "modal-body"; - dialog.appendChild(body); - - // The footer. - var footer = doc.createElement("div"); - footer.className = "modal-footer"; - dialog.appendChild(footer); - - // The dialog text. - var question = doc.createElement("p"); - question.innerHTML = text; - question.style.padding = "5px"; - body.appendChild(question); - - // The web form container for the text box and buttons. - var form = doc.createElement("form"), - style = form.style; - form.onsubmit = function () { return close(false); }; - style.padding = "0"; - style.margin = "0"; - body.appendChild(form); - - // The input text box - input = doc.createElement("input"); - input.type = "text"; - input.value = defaultInputText; - style = input.style; - style.display = "block"; - style.width = "80%"; - style.marginLeft = style.marginRight = "auto"; - form.appendChild(input); - - // The ok button - var okButton = doc.createElement("button"); - okButton.className = "btn btn-primary"; - okButton.type = "button"; - okButton.onclick = function () { return close(false); }; - okButton.innerHTML = "OK"; - - // The cancel button - var cancelButton = doc.createElement("button"); - cancelButton.className = "btn btn-primary"; - cancelButton.type = "button"; - cancelButton.onclick = function () { return close(true); }; - cancelButton.innerHTML = "Cancel"; - - footer.appendChild(okButton); - footer.appendChild(cancelButton); - - util.addEvent(doc.body, "keydown", checkEscape); - - doc.body.appendChild(dialog); - - }; - - // Why is this in a zero-length timeout? - // Is it working around a browser bug? - setTimeout(function () { - - createDialog(); - - var defTextLen = defaultInputText.length; - if (input.selectionStart !== undefined) { - input.selectionStart = 0; - input.selectionEnd = defTextLen; - } - else if (input.createTextRange) { - var range = input.createTextRange(); - range.collapse(false); - range.moveStart("character", -defTextLen); - range.moveEnd("character", defTextLen); - range.select(); - } - - $(dialog).on('shown', function () { - input.focus(); - }) - - $(dialog).on('hidden', function () { - dialog.parentNode.removeChild(dialog); - }) - - $(dialog).modal() - - }, 0); - }; - - function UIManager(postfix, panels, undoManager, previewManager, commandManager, helpOptions) { - - var inputBox = panels.input, - buttons = {}; // buttons.undo, buttons.link, etc. The actual DOM elements. - - makeSpritedButtonRow(); - - var keyEvent = "keydown"; - if (uaSniffed.isOpera) { - keyEvent = "keypress"; - } - - util.addEvent(inputBox, keyEvent, function (key) { - - // Check to see if we have a button key and, if so execute the callback. - if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) { - - var keyCode = key.charCode || key.keyCode; - var keyCodeStr = String.fromCharCode(keyCode).toLowerCase(); - - switch (keyCodeStr) { - case "b": - doClick(buttons.bold); - break; - case "i": - doClick(buttons.italic); - break; - case "l": - doClick(buttons.link); - break; - case "q": - doClick(buttons.quote); - break; - case "k": - doClick(buttons.code); - break; - case "g": - doClick(buttons.image); - break; - case "o": - doClick(buttons.olist); - break; - case "u": - doClick(buttons.ulist); - break; - case "h": - doClick(buttons.heading); - break; - case "r": - doClick(buttons.hr); - break; - case "y": - doClick(buttons.redo); - break; - case "z": - if (key.shiftKey) { - doClick(buttons.redo); - } - else { - doClick(buttons.undo); - } - break; - default: - return; - } - - - if (key.preventDefault) { - key.preventDefault(); - } - - if (window.event) { - window.event.returnValue = false; - } - } - }); - - // Auto-indent on shift-enter - util.addEvent(inputBox, "keyup", function (key) { - if (key.shiftKey && !key.ctrlKey && !key.metaKey) { - var keyCode = key.charCode || key.keyCode; - // Character 13 is Enter - if (keyCode === 13) { - var fakeButton = {}; - fakeButton.textOp = bindCommand("doAutoindent"); - doClick(fakeButton); - } - } - }); - - // special handler because IE clears the context of the textbox on ESC - if (uaSniffed.isIE) { - util.addEvent(inputBox, "keydown", function (key) { - var code = key.keyCode; - if (code === 27) { - return false; - } - }); - } - - - // Perform the button's action. - function doClick(button) { - - inputBox.focus(); - - if (button.textOp) { - - if (undoManager) { - undoManager.setCommandMode(); - } - - var state = new TextareaState(panels); - - if (!state) { - return; - } - - var chunks = state.getChunks(); - - // Some commands launch a "modal" prompt dialog. Javascript - // can't really make a modal dialog box and the WMD code - // will continue to execute while the dialog is displayed. - // This prevents the dialog pattern I'm used to and means - // I can't do something like this: - // - // var link = CreateLinkDialog(); - // makeMarkdownLink(link); - // - // Instead of this straightforward method of handling a - // dialog I have to pass any code which would execute - // after the dialog is dismissed (e.g. link creation) - // in a function parameter. - // - // Yes this is awkward and I think it sucks, but there's - // no real workaround. Only the image and link code - // create dialogs and require the function pointers. - var fixupInputArea = function () { - - inputBox.focus(); - - if (chunks) { - state.setChunks(chunks); - } - - state.restore(); - previewManager.refresh(); - }; - - var noCleanup = button.textOp(chunks, fixupInputArea); - - if (!noCleanup) { - fixupInputArea(); - } - - } - - if (button.execute) { - button.execute(undoManager); - } - }; - - function setupButton(button, isEnabled) { - - if (isEnabled) { - button.disabled = false; - - if (!button.isHelp) { - button.onclick = function () { - if (this.onmouseout) { - this.onmouseout(); - } - doClick(this); - return false; - } - } - } - else { - button.disabled = true; - } - } - - function bindCommand(method) { - if (typeof method === "string") - method = commandManager[method]; - return function () { method.apply(commandManager, arguments); } - } - - function makeSpritedButtonRow() { - - var buttonBar = panels.buttonBar; - var buttonRow = document.createElement("div"); - buttonRow.id = "wmd-button-row" + postfix; - buttonRow.className = 'btn-toolbar'; - buttonRow = buttonBar.appendChild(buttonRow); - - var makeButton = function (id, title, icon, textOp, group) { - var button = document.createElement("button"); - button.className = "btn"; - var buttonImage = document.createElement("i"); - buttonImage.className = icon; - button.id = id + postfix; - button.appendChild(buttonImage); - button.title = title; - $(button).tooltip({placement: 'bottom'}) - if (textOp) - button.textOp = textOp; - setupButton(button, true); - if (group) { - group.appendChild(button); - } else { - buttonRow.appendChild(button); - } - return button; - }; - var makeGroup = function (num) { - var group = document.createElement("div"); - group.className = "btn-group wmd-button-group" + num; - group.id = "wmd-button-group" + num + postfix; - buttonRow.appendChild(group); - return group - } - - group1 = makeGroup(1); - buttons.bold = makeButton("wmd-bold-button", "Bold - Ctrl+B", "fa fa-bold", bindCommand("doBold"), group1); - buttons.italic = makeButton("wmd-italic-button", "Italic - Ctrl+I", "fa fa-italic", bindCommand("doItalic"), group1); - - group2 = makeGroup(2); - /* - buttons.link = makeButton("wmd-link-button", "Link - Ctrl+L", "fa fa-link", bindCommand(function (chunk, postProcessing) { - return this.doLinkOrImage(chunk, postProcessing, false); - }), group2); - */ - buttons.quote = makeButton("wmd-quote-button", "Blockquote - Ctrl+Q", "fa fa-quote-left", bindCommand("doBlockquote"), group2); - buttons.code = makeButton("wmd-code-button", "Code Sample - Ctrl+K", "fa fa-code", bindCommand("doCode"), group2); - /* - buttons.image = makeButton("wmd-image-button", "Image - Ctrl+G", "fa fa-picture", bindCommand(function (chunk, postProcessing) { - return this.doLinkOrImage(chunk, postProcessing, true); - }), group2); - */ - - group3 = makeGroup(3); - buttons.olist = makeButton("wmd-olist-button", "Numbered List - Ctrl+O", "fa fa-list", bindCommand(function (chunk, postProcessing) { - this.doList(chunk, postProcessing, true); - }), group3); - buttons.ulist = makeButton("wmd-ulist-button", "Bulleted List - Ctrl+U", "fa fa-list-ul", bindCommand(function (chunk, postProcessing) { - this.doList(chunk, postProcessing, false); - }), group3); - buttons.heading = makeButton("wmd-heading-button", "Heading - Ctrl+H", "fa fa-tasks", bindCommand("doHeading"), group3); - buttons.hr = makeButton("wmd-hr-button", "Horizontal Rule - Ctrl+R", "fa fa-minus", bindCommand("doHorizontalRule"), group3); - - group4 = makeGroup(4); - buttons.undo = makeButton("wmd-undo-button", "Undo - Ctrl+Z", "fa fa-undo", null, group4); - buttons.undo.execute = function (manager) { if (manager) manager.undo(); }; - - var redoTitle = /win/.test(nav.platform.toLowerCase()) ? - "Redo - Ctrl+Y" : - "Redo - Ctrl+Shift+Z"; // mac and other non-Windows platforms - - buttons.redo = makeButton("wmd-redo-button", redoTitle, "fa fa-share", null, group4); - buttons.redo.execute = function (manager) { if (manager) manager.redo(); }; - - if (helpOptions) { - group5 = makeGroup(5); - group5.className = group5.className + " pull-right"; - var helpButton = document.createElement("button"); - var helpButtonImage = document.createElement("i"); - helpButtonImage.className = "fa fa-question-sign"; - helpButton.appendChild(helpButtonImage); - helpButton.className = "btn"; - helpButton.id = "wmd-help-button" + postfix; - helpButton.isHelp = true; - helpButton.title = helpOptions.title || defaultHelpHoverTitle; - $(helpButton).tooltip({placement: 'bottom'}) - helpButton.onclick = helpOptions.handler; - - setupButton(helpButton, true); - group5.appendChild(helpButton); - buttons.help = helpButton; - } - - setUndoRedoButtonStates(); - } - - function setUndoRedoButtonStates() { - if (undoManager) { - setupButton(buttons.undo, undoManager.canUndo()); - setupButton(buttons.redo, undoManager.canRedo()); - } - }; - - this.setUndoRedoButtonStates = setUndoRedoButtonStates; - - } - - function CommandManager(pluginHooks) { - this.hooks = pluginHooks; - } - - var commandProto = CommandManager.prototype; - - // The markdown symbols - 4 spaces = code, > = blockquote, etc. - commandProto.prefixes = "(?:\\s{4,}|\\s*>|\\s*-\\s+|\\s*\\d+\\.|=|\\+|-|_|\\*|#|\\s*\\[[^\n]]+\\]:)"; - - // Remove markdown symbols from the chunk selection. - commandProto.unwrap = function (chunk) { - var txt = new re("([^\\n])\\n(?!(\\n|" + this.prefixes + "))", "g"); - chunk.selection = chunk.selection.replace(txt, "$1 $2"); - }; - - commandProto.wrap = function (chunk, len) { - this.unwrap(chunk); - var regex = new re("(.{1," + len + "})( +|$\\n?)", "gm"), - that = this; - - chunk.selection = chunk.selection.replace(regex, function (line, marked) { - if (new re("^" + that.prefixes, "").test(line)) { - return line; - } - return marked + "\n"; - }); - - chunk.selection = chunk.selection.replace(/\s+$/, ""); - }; - - commandProto.doBold = function (chunk, postProcessing) { - return this.doBorI(chunk, postProcessing, 2, "strong text"); - }; - - commandProto.doItalic = function (chunk, postProcessing) { - return this.doBorI(chunk, postProcessing, 1, "emphasized text"); - }; - - // chunk: The selected region that will be enclosed with */** - // nStars: 1 for italics, 2 for bold - // insertText: If you just click the button without highlighting text, this gets inserted - commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) { - - // Get rid of whitespace and fixup newlines. - chunk.trimWhitespace(); - chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n"); - - // Look for stars before and after. Is the chunk already marked up? - // note that these regex matches cannot fail - var starsBefore = /(\**$)/.exec(chunk.before)[0]; - var starsAfter = /(^\**)/.exec(chunk.after)[0]; - - var prevStars = Math.min(starsBefore.length, starsAfter.length); - - // Remove stars if we have to since the button acts as a toggle. - if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) { - chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), ""); - chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), ""); - } - else if (!chunk.selection && starsAfter) { - // It's not really clear why this code is necessary. It just moves - // some arbitrary stuff around. - chunk.after = chunk.after.replace(/^([*_]*)/, ""); - chunk.before = chunk.before.replace(/(\s?)$/, ""); - var whitespace = re.$1; - chunk.before = chunk.before + starsAfter + whitespace; - } - else { - - // In most cases, if you don't have any selected text and click the button - // you'll get a selected, marked up region with the default text inserted. - if (!chunk.selection && !starsAfter) { - chunk.selection = insertText; - } - - // Add the true markup. - var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ? - chunk.before = chunk.before + markup; - chunk.after = markup + chunk.after; - } - - return; - }; - - commandProto.stripLinkDefs = function (text, defsToAdd) { - - text = text.replace(/^[ ]{0,3}\[(\d+)\]:[ \t]*\n?[ \t]*?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|$)/gm, - function (totalMatch, id, link, newlines, title) { - defsToAdd[id] = totalMatch.replace(/\s*$/, ""); - if (newlines) { - // Strip the title and return that separately. - defsToAdd[id] = totalMatch.replace(/["(](.+?)[")]$/, ""); - return newlines + title; - } - return ""; - }); - - return text; - }; - - commandProto.addLinkDef = function (chunk, linkDef) { - - var refNumber = 0; // The current reference number - var defsToAdd = {}; // - // Start with a clean slate by removing all previous link definitions. - chunk.before = this.stripLinkDefs(chunk.before, defsToAdd); - chunk.selection = this.stripLinkDefs(chunk.selection, defsToAdd); - chunk.after = this.stripLinkDefs(chunk.after, defsToAdd); - - var defs = ""; - var regex = /(\[)((?:\[[^\]]*\]|[^\[\]])*)(\][ ]?(?:\n[ ]*)?\[)(\d+)(\])/g; - - var addDefNumber = function (def) { - refNumber++; - def = def.replace(/^[ ]{0,3}\[(\d+)\]:/, " [" + refNumber + "]:"); - defs += "\n" + def; - }; - - // note that - // a) the recursive call to getLink cannot go infinite, because by definition - // of regex, inner is always a proper substring of wholeMatch, and - // b) more than one level of nesting is neither supported by the regex - // nor making a lot of sense (the only use case for nesting is a linked image) - var getLink = function (wholeMatch, before, inner, afterInner, id, end) { - inner = inner.replace(regex, getLink); - if (defsToAdd[id]) { - addDefNumber(defsToAdd[id]); - return before + inner + afterInner + refNumber + end; - } - return wholeMatch; - }; - - chunk.before = chunk.before.replace(regex, getLink); - - if (linkDef) { - addDefNumber(linkDef); - } - else { - chunk.selection = chunk.selection.replace(regex, getLink); - } - - var refOut = refNumber; - - chunk.after = chunk.after.replace(regex, getLink); - - if (chunk.after) { - chunk.after = chunk.after.replace(/\n*$/, ""); - } - if (!chunk.after) { - chunk.selection = chunk.selection.replace(/\n*$/, ""); - } - - chunk.after += "\n\n" + defs; - - return refOut; - }; - - // takes the line as entered into the add link/as image dialog and makes - // sure the URL and the optinal title are "nice". - function properlyEncoded(linkdef) { - return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) { - link = link.replace(/\?.*$/, function (querypart) { - return querypart.replace(/\+/g, " "); // in the query string, a plus and a space are identical - }); - link = decodeURIComponent(link); // unencode first, to prevent double encoding - link = encodeURI(link).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29'); - link = link.replace(/\?.*$/, function (querypart) { - return querypart.replace(/\+/g, "%2b"); // since we replaced plus with spaces in the query part, all pluses that now appear where originally encoded - }); - if (title) { - title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, ""); - title = $.trim(title).replace(/"/g, "quot;").replace(/\(/g, "(").replace(/\)/g, ")").replace(//g, ">"); - } - return title ? link + ' "' + title + '"' : link; - }); - } - - commandProto.doLinkOrImage = function (chunk, postProcessing, isImage) { - - chunk.trimWhitespace(); - chunk.findTags(/\s*!?\[/, /\][ ]?(?:\n[ ]*)?(\[.*?\])?/); - var background; - - if (chunk.endTag.length > 1 && chunk.startTag.length > 0) { - - chunk.startTag = chunk.startTag.replace(/!?\[/, ""); - chunk.endTag = ""; - this.addLinkDef(chunk, null); - - } - else { - - // We're moving start and end tag back into the selection, since (as we're in the else block) we're not - // *removing* a link, but *adding* one, so whatever findTags() found is now back to being part of the - // link text. linkEnteredCallback takes care of escaping any brackets. - chunk.selection = chunk.startTag + chunk.selection + chunk.endTag; - chunk.startTag = chunk.endTag = ""; - - if (/\n\n/.test(chunk.selection)) { - this.addLinkDef(chunk, null); - return; - } - var that = this; - // The function to be executed when you enter a link and press OK or Cancel. - // Marks up the link and adds the ref. - var linkEnteredCallback = function (link) { - - if (link !== null) { - // ( $1 - // [^\\] anything that's not a backslash - // (?:\\\\)* an even number (this includes zero) of backslashes - // ) - // (?= followed by - // [[\]] an opening or closing bracket - // ) - // - // In other words, a non-escaped bracket. These have to be escaped now to make sure they - // don't count as the end of the link or similar. - // Note that the actual bracket has to be a lookahead, because (in case of to subsequent brackets), - // the bracket in one match may be the "not a backslash" character in the next match, so it - // should not be consumed by the first match. - // The "prepend a space and finally remove it" steps makes sure there is a "not a backslash" at the - // start of the string, so this also works if the selection begins with a bracket. We cannot solve - // this by anchoring with ^, because in the case that the selection starts with two brackets, this - // would mean a zero-width match at the start. Since zero-width matches advance the string position, - // the first bracket could then not act as the "not a backslash" for the second. - chunk.selection = (" " + chunk.selection).replace(/([^\\](?:\\\\)*)(?=[[\]])/g, "$1\\").substr(1); - - var linkDef = " [999]: " + properlyEncoded(link); - - var num = that.addLinkDef(chunk, linkDef); - chunk.startTag = isImage ? "![" : "["; - chunk.endTag = "][" + num + "]"; - - if (!chunk.selection) { - if (isImage) { - chunk.selection = "enter image description here"; - } - else { - chunk.selection = "enter link description here"; - } - } - } - postProcessing(); - }; - - - if (isImage) { - if (!this.hooks.insertImageDialog(linkEnteredCallback)) - ui.prompt('Insert Image', imageDialogText, imageDefaultText, linkEnteredCallback); - } - else { - ui.prompt('Insert Link', linkDialogText, linkDefaultText, linkEnteredCallback); - } - return true; - } - }; - - // When making a list, hitting shift-enter will put your cursor on the next line - // at the current indent level. - commandProto.doAutoindent = function (chunk, postProcessing) { - - var commandMgr = this, - fakeSelection = false; - - chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n"); - chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n"); - chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n"); - - // There's no selection, end the cursor wasn't at the end of the line: - // The user wants to split the current list item / code line / blockquote line - // (for the latter it doesn't really matter) in two. Temporarily select the - // (rest of the) line to achieve this. - if (!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) { - chunk.after = chunk.after.replace(/^[^\n]*/, function (wholeMatch) { - chunk.selection = wholeMatch; - return ""; - }); - fakeSelection = true; - } - - if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) { - if (commandMgr.doList) { - commandMgr.doList(chunk); - } - } - if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) { - if (commandMgr.doBlockquote) { - commandMgr.doBlockquote(chunk); - } - } - if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { - if (commandMgr.doCode) { - commandMgr.doCode(chunk); - } - } - - if (fakeSelection) { - chunk.after = chunk.selection + chunk.after; - chunk.selection = ""; - } - }; - - commandProto.doBlockquote = function (chunk, postProcessing) { - - chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/, - function (totalMatch, newlinesBefore, text, newlinesAfter) { - chunk.before += newlinesBefore; - chunk.after = newlinesAfter + chunk.after; - return text; - }); - - chunk.before = chunk.before.replace(/(>[ \t]*)$/, - function (totalMatch, blankLine) { - chunk.selection = blankLine + chunk.selection; - return ""; - }); - - chunk.selection = chunk.selection.replace(/^(\s|>)+$/, ""); - chunk.selection = chunk.selection || "Blockquote"; - - // The original code uses a regular expression to find out how much of the - // text *directly before* the selection already was a blockquote: - - /* - if (chunk.before) { - chunk.before = chunk.before.replace(/\n?$/, "\n"); - } - chunk.before = chunk.before.replace(/(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*$)/, - function (totalMatch) { - chunk.startTag = totalMatch; - return ""; - }); - */ - - // This comes down to: - // Go backwards as many lines a possible, such that each line - // a) starts with ">", or - // b) is almost empty, except for whitespace, or - // c) is preceeded by an unbroken chain of non-empty lines - // leading up to a line that starts with ">" and at least one more character - // and in addition - // d) at least one line fulfills a) - // - // Since this is essentially a backwards-moving regex, it's susceptible to - // catstrophic backtracking and can cause the browser to hang; - // see e.g. http://meta.stackoverflow.com/questions/9807. - // - // Hence we replaced this by a simple state machine that just goes through the - // lines and checks for a), b), and c). - - var match = "", - leftOver = "", - line; - if (chunk.before) { - var lines = chunk.before.replace(/\n$/, "").split("\n"); - var inChain = false; - for (var i = 0; i < lines.length; i++) { - var good = false; - line = lines[i]; - inChain = inChain && line.length > 0; // c) any non-empty line continues the chain - if (/^>/.test(line)) { // a) - good = true; - if (!inChain && line.length > 1) // c) any line that starts with ">" and has at least one more character starts the chain - inChain = true; - } else if (/^[ \t]*$/.test(line)) { // b) - good = true; - } else { - good = inChain; // c) the line is not empty and does not start with ">", so it matches if and only if we're in the chain - } - if (good) { - match += line + "\n"; - } else { - leftOver += match + line; - match = "\n"; - } - } - if (!/(^|\n)>/.test(match)) { // d) - leftOver += match; - match = ""; - } - } - - chunk.startTag = match; - chunk.before = leftOver; - - // end of change - - if (chunk.after) { - chunk.after = chunk.after.replace(/^\n?/, "\n"); - } - - chunk.after = chunk.after.replace(/^(((\n|^)(\n[ \t]*)*>(.+\n)*.*)+(\n[ \t]*)*)/, - function (totalMatch) { - chunk.endTag = totalMatch; - return ""; - } - ); - - var replaceBlanksInTags = function (useBracket) { - - var replacement = useBracket ? "> " : ""; - - if (chunk.startTag) { - chunk.startTag = chunk.startTag.replace(/\n((>|\s)*)\n$/, - function (totalMatch, markdown) { - return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n"; - }); - } - if (chunk.endTag) { - chunk.endTag = chunk.endTag.replace(/^\n((>|\s)*)\n/, - function (totalMatch, markdown) { - return "\n" + markdown.replace(/^[ ]{0,3}>?[ \t]*$/gm, replacement) + "\n"; - }); - } - }; - - if (/^(?![ ]{0,3}>)/m.test(chunk.selection)) { - this.wrap(chunk, SETTINGS.lineLength - 2); - chunk.selection = chunk.selection.replace(/^/gm, "> "); - replaceBlanksInTags(true); - chunk.skipLines(); - } else { - chunk.selection = chunk.selection.replace(/^[ ]{0,3}> ?/gm, ""); - this.unwrap(chunk); - replaceBlanksInTags(false); - - if (!/^(\n|^)[ ]{0,3}>/.test(chunk.selection) && chunk.startTag) { - chunk.startTag = chunk.startTag.replace(/\n{0,2}$/, "\n\n"); - } - - if (!/(\n|^)[ ]{0,3}>.*$/.test(chunk.selection) && chunk.endTag) { - chunk.endTag = chunk.endTag.replace(/^\n{0,2}/, "\n\n"); - } - } - - chunk.selection = this.hooks.postBlockquoteCreation(chunk.selection); - - if (!/\n/.test(chunk.selection)) { - chunk.selection = chunk.selection.replace(/^(> *)/, - function (wholeMatch, blanks) { - chunk.startTag += blanks; - return ""; - }); - } - }; - - commandProto.doCode = function (chunk, postProcessing) { - - var hasTextBefore = /\S[ ]*$/.test(chunk.before); - var hasTextAfter = /^[ ]*\S/.test(chunk.after); - - // Use 'four space' markdown if the selection is on its own - // line or is multiline. - if ((!hasTextAfter && !hasTextBefore) || /\n/.test(chunk.selection)) { - - chunk.before = chunk.before.replace(/[ ]{4}$/, - function (totalMatch) { - chunk.selection = totalMatch + chunk.selection; - return ""; - }); - - var nLinesBack = 1; - var nLinesForward = 1; - - if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) { - nLinesBack = 0; - } - if (/^\n(\t|[ ]{4,})/.test(chunk.after)) { - nLinesForward = 0; - } - - chunk.skipLines(nLinesBack, nLinesForward); - - if (!chunk.selection) { - chunk.startTag = " "; - chunk.selection = "enter code here"; - } - else { - if (/^[ ]{0,3}\S/m.test(chunk.selection)) { - if (/\n/.test(chunk.selection)) - chunk.selection = chunk.selection.replace(/^/gm, " "); - else // if it's not multiline, do not select the four added spaces; this is more consistent with the doList behavior - chunk.before += " "; - } - else { - chunk.selection = chunk.selection.replace(/^[ ]{4}/gm, ""); - } - } - } - else { - // Use backticks (`) to delimit the code block. - - chunk.trimWhitespace(); - chunk.findTags(/`/, /`/); - - if (!chunk.startTag && !chunk.endTag) { - chunk.startTag = chunk.endTag = "`"; - if (!chunk.selection) { - chunk.selection = "enter code here"; - } - } - else if (chunk.endTag && !chunk.startTag) { - chunk.before += chunk.endTag; - chunk.endTag = ""; - } - else { - chunk.startTag = chunk.endTag = ""; - } - } - }; - - commandProto.doList = function (chunk, postProcessing, isNumberedList) { - - // These are identical except at the very beginning and end. - // Should probably use the regex extension function to make this clearer. - var previousItemsRegex = /(\n|^)(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*$/; - var nextItemsRegex = /^\n*(([ ]{0,3}([*+-]|\d+[.])[ \t]+.*)(\n.+|\n{2,}([*+-].*|\d+[.])[ \t]+.*|\n{2,}[ \t]+\S.*)*)\n*/; - - // The default bullet is a dash but others are possible. - // This has nothing to do with the particular HTML bullet, - // it's just a markdown bullet. - var bullet = "-"; - - // The number in a numbered list. - var num = 1; - - // Get the item prefix - e.g. " 1. " for a numbered list, " - " for a bulleted list. - var getItemPrefix = function () { - var prefix; - if (isNumberedList) { - prefix = " " + num + ". "; - num++; - } - else { - prefix = " " + bullet + " "; - } - return prefix; - }; - - // Fixes the prefixes of the other list items. - var getPrefixedItem = function (itemText) { - - // The numbering flag is unset when called by autoindent. - if (isNumberedList === undefined) { - isNumberedList = /^\s*\d/.test(itemText); - } - - // Renumber/bullet the list element. - itemText = itemText.replace(/^[ ]{0,3}([*+-]|\d+[.])\s/gm, - function (_) { - return getItemPrefix(); - }); - - return itemText; - }; - - chunk.findTags(/(\n|^)*[ ]{0,3}([*+-]|\d+[.])\s+/, null); - - if (chunk.before && !/\n$/.test(chunk.before) && !/^\n/.test(chunk.startTag)) { - chunk.before += chunk.startTag; - chunk.startTag = ""; - } - - if (chunk.startTag) { - - var hasDigits = /\d+[.]/.test(chunk.startTag); - chunk.startTag = ""; - chunk.selection = chunk.selection.replace(/\n[ ]{4}/g, "\n"); - this.unwrap(chunk); - chunk.skipLines(); - - if (hasDigits) { - // Have to renumber the bullet points if this is a numbered list. - chunk.after = chunk.after.replace(nextItemsRegex, getPrefixedItem); - } - if (isNumberedList == hasDigits) { - return; - } - } - - var nLinesUp = 1; - - chunk.before = chunk.before.replace(previousItemsRegex, - function (itemText) { - if (/^\s*([*+-])/.test(itemText)) { - bullet = re.$1; - } - nLinesUp = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0; - return getPrefixedItem(itemText); - }); - - if (!chunk.selection) { - chunk.selection = "List item"; - } - - var prefix = getItemPrefix(); - - var nLinesDown = 1; - - chunk.after = chunk.after.replace(nextItemsRegex, - function (itemText) { - nLinesDown = /[^\n]\n\n[^\n]/.test(itemText) ? 1 : 0; - return getPrefixedItem(itemText); - }); - - chunk.trimWhitespace(true); - chunk.skipLines(nLinesUp, nLinesDown, true); - chunk.startTag = prefix; - var spaces = prefix.replace(/./g, " "); - this.wrap(chunk, SETTINGS.lineLength - spaces.length); - chunk.selection = chunk.selection.replace(/\n/g, "\n" + spaces); - - }; - - commandProto.doHeading = function (chunk, postProcessing) { - - // Remove leading/trailing whitespace and reduce internal spaces to single spaces. - chunk.selection = chunk.selection.replace(/\s+/g, " "); - chunk.selection = chunk.selection.replace(/(^\s+|\s+$)/g, ""); - - // If we clicked the button with no selected text, we just - // make a level 2 hash header around some default text. - if (!chunk.selection) { - chunk.startTag = "## "; - chunk.selection = "Heading"; - chunk.endTag = " ##"; - return; - } - - var headerLevel = 0; // The existing header level of the selected text. - - // Remove any existing hash heading markdown and save the header level. - chunk.findTags(/#+[ ]*/, /[ ]*#+/); - if (/#+/.test(chunk.startTag)) { - headerLevel = re.lastMatch.length; - } - chunk.startTag = chunk.endTag = ""; - - // Try to get the current header level by looking for - and = in the line - // below the selection. - chunk.findTags(null, /\s?(-+|=+)/); - if (/=+/.test(chunk.endTag)) { - headerLevel = 1; - } - if (/-+/.test(chunk.endTag)) { - headerLevel = 2; - } - - // Skip to the next line so we can create the header markdown. - chunk.startTag = chunk.endTag = ""; - chunk.skipLines(1, 1); - - // We make a level 2 header if there is no current header. - // If there is a header level, we substract one from the header level. - // If it's already a level 1 header, it's removed. - var headerLevelToCreate = headerLevel == 0 ? 2 : headerLevel - 1; - - if (headerLevelToCreate > 0) { - - // The button only creates level 1 and 2 underline headers. - // Why not have it iterate over hash header levels? Wouldn't that be easier and cleaner? - var headerChar = headerLevelToCreate >= 2 ? "-" : "="; - var len = chunk.selection.length; - if (len > SETTINGS.lineLength) { - len = SETTINGS.lineLength; - } - chunk.endTag = "\n"; - while (len--) { - chunk.endTag += headerChar; - } - } - }; - - commandProto.doHorizontalRule = function (chunk, postProcessing) { - chunk.startTag = "----------\n"; - chunk.selection = ""; - chunk.skipLines(2, 1, true); - } - - -})(); \ No newline at end of file diff --git a/static/lib/pagedown/Markdown.Editor.less b/static/lib/pagedown/Markdown.Editor.less deleted file mode 100755 index e1c0cc4df..000000000 --- a/static/lib/pagedown/Markdown.Editor.less +++ /dev/null @@ -1,80 +0,0 @@ - -.wmd-panel { - width: 100%; -} - -.wmd-input { - height: 300px; - width: 100%; - box-sizing: border-box; - -webkit-box-sizing:border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; -} - -.wmd-preview { - .well; - width: 100%; - box-sizing: border-box; - -webkit-box-sizing:border-box; - -moz-box-sizing: border-box; - -ms-box-sizing: border-box; -} - -.wmd-panel .btn-toolbar { - margin-bottom: 0; - padding: 0; - width: 100%; -} - -.icon-link, -.icon-blockquote, -.icon-code, -.icon-bullet-list, -.icon-list, -.icon-header, -.icon-hr-line, -.icon-undo { - background-image: url(Markdown.Editor.Icons.png); -} -.icon-link { background-position: 0 0; } -.icon-blockquote { background-position: -24px 0; } -.icon-code { background-position: -48px 0; } -.icon-bullet-list { background-position: -72px 0; } -.icon-list { background-position: -96px 0; } -.icon-header { background-position: -120px 0; } -.icon-hr-line { background-position: -144px 0; } -.icon-undo { background-position: -168px 0; } - - - - - -.wmd-prompt-background -{ - background-color: Black; -} - -.wmd-prompt-dialog -{ - border: 1px solid #999999; - background-color: #F5F5F5; -} - -.wmd-prompt-dialog > div { - font-size: 0.8em; - font-family: arial, helvetica, sans-serif; -} - - -.wmd-prompt-dialog > form > input[type="text"] { - border: 1px solid #999999; - color: black; -} - -.wmd-prompt-dialog > form > input[type="button"]{ - border: 1px solid #888888; - font-family: trebuchet MS, helvetica, sans-serif; - font-size: 0.8em; - font-weight: bold; -} diff --git a/static/lib/pagedown/Markdown.Sanitizer.js b/static/lib/pagedown/Markdown.Sanitizer.js deleted file mode 100755 index be0445ab5..000000000 --- a/static/lib/pagedown/Markdown.Sanitizer.js +++ /dev/null @@ -1,111 +0,0 @@ -(function () { - var output, Converter; - if (typeof exports === "object" && typeof require === "function") { // we're in a CommonJS (e.g. Node.js) module - output = exports; - Converter = require("./Markdown.Converter").Converter; - } else { - output = window.Markdown; - Converter = output.Converter; - } - - output.getSanitizingConverter = function () { - var converter = new Converter(); - converter.hooks.chain("postConversion", sanitizeHtml); - converter.hooks.chain("postConversion", balanceTags); - return converter; - } - - function sanitizeHtml(html) { - return html.replace(/<[^>]*>?/gi, sanitizeTag); - } - - // (tags that can be opened/closed) | (tags that stand alone) - var basic_tag_whitelist = /^(<\/?(b|blockquote|code|del|dd|dl|dt|em|h1|h2|h3|i|kbd|li|ol|p|s|sup|sub|strong|strike|ul)>|<(br|hr)\s?\/?>)$/i; - // | - var a_white = /^(]+")?\s?>|<\/a>)$/i; - - // ]*")?(\stitle="[^"<>]*")?\s?\/?>)$/i; - - //
    |
    for twitter bootstrap - var pre_white = /^(|<\/pre>)$/i; - - function sanitizeTag(tag) { - if (tag.match(basic_tag_whitelist) || tag.match(a_white) || tag.match(img_white) || tag.match(pre_white)) - return tag; - else - return ""; - } - - /// - /// attempt to balance HTML tags in the html string - /// by removing any unmatched opening or closing tags - /// IMPORTANT: we *assume* HTML has *already* been - /// sanitized and is safe/sane before balancing! - /// - /// adapted from CODESNIPPET: A8591DBA-D1D3-11DE-947C-BA5556D89593 - /// - function balanceTags(html) { - - if (html == "") - return ""; - - var re = /<\/?\w+[^>]*(\s|$|>)/g; - // convert everything to lower case; this makes - // our case insensitive comparisons easier - var tags = html.toLowerCase().match(re); - - // no HTML tags present? nothing to do; exit now - var tagcount = (tags || []).length; - if (tagcount == 0) - return html; - - var tagname, tag; - var ignoredtags = "



  • "; - var match; - var tagpaired = []; - var tagremove = []; - var needsRemoval = false; - - // loop through matched tags in forward order - for (var ctag = 0; ctag < tagcount; ctag++) { - tagname = tags[ctag].replace(/<\/?(\w+).*/, "$1"); - // skip any already paired tags - // and skip tags in our ignore list; assume they're self-closed - if (tagpaired[ctag] || ignoredtags.search("<" + tagname + ">") > -1) - continue; - - tag = tags[ctag]; - match = -1; - - if (!/^<\//.test(tag)) { - // this is an opening tag - // search forwards (next tags), look for closing tags - for (var ntag = ctag + 1; ntag < tagcount; ntag++) { - if (!tagpaired[ntag] && tags[ntag] == "") { - match = ntag; - break; - } - } - } - - if (match == -1) - needsRemoval = tagremove[ctag] = true; // mark for removal - else - tagpaired[match] = true; // mark paired - } - - if (!needsRemoval) - return html; - - // delete all orphaned tags from the string - - var ctag = 0; - html = html.replace(re, function (match) { - var res = tagremove[ctag] ? "" : match; - ctag++; - return res; - }); - return html; - } -})(); diff --git a/static/partials/new-repo.html b/static/partials/new-repo.html index 6eac7a55e..12763b15e 100644 --- a/static/partials/new-repo.html +++ b/static/partials/new-repo.html @@ -63,8 +63,12 @@
    Repository Description

    -
    +
    + +
  • diff --git a/static/partials/search.html b/static/partials/search.html index d1f35905e..c800736e3 100644 --- a/static/partials/search.html +++ b/static/partials/search.html @@ -25,7 +25,8 @@ {{ result.namespace.name }}/{{ result.name }}

    - +

    Last Modified: diff --git a/static/partials/super-user.html b/static/partials/super-user.html index 90fff94fe..7ade80b23 100644 --- a/static/partials/super-user.html +++ b/static/partials/super-user.html @@ -120,7 +120,8 @@

    Change Log

    -
    + diff --git a/static/partials/team-view.html b/static/partials/team-view.html index b046f8d2b..1e3d7834f 100644 --- a/static/partials/team-view.html +++ b/static/partials/team-view.html @@ -59,10 +59,12 @@
    Team Description
    -
    +
    + +
    diff --git a/webpack.config.js b/webpack.config.js index 3c5c13a57..ec2e89b18 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -16,7 +16,7 @@ var config = { // Use window.angular to maintain compatibility with non-Webpack components externals: { angular: "angular", - jquery: "$", + jquery: "$" }, module: { rules: [ @@ -28,13 +28,12 @@ var config = { exclude: /node_modules/ }, { - test: /\.scss$/, + test: /\.s?css$/, use: [ "style-loader", "css-loader", "sass-loader", ], - exclude: /node_modules/ }, ] }, diff --git a/yarn.lock b/yarn.lock index c9261bb5a..b8e88f180 100644 --- a/yarn.lock +++ b/yarn.lock @@ -50,6 +50,10 @@ version "0.14.39" resolved "https://registry.yarnpkg.com/@types/react/-/react-0.14.39.tgz#11cb715768da5f7605aa2030a5dc63e77a137eb5" +"@types/showdown@^1.4.32": + version "1.4.32" + resolved "https://registry.yarnpkg.com/@types/showdown/-/showdown-1.4.32.tgz#bb0b32dbafee23ae9575df30b227e4fc2f0cd45b" + abbrev@1, abbrev@1.0.x: version "1.0.9" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" @@ -1634,6 +1638,10 @@ hawk@~3.1.3: hoek "2.x.x" sntp "1.x.x" +highlightjs@^9.8.0: + version "9.10.0" + resolved "https://registry.yarnpkg.com/highlightjs/-/highlightjs-9.10.0.tgz#fca9b78ddaa3b1abca89d6c3ee105ad270a80190" + hmac-drbg@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.0.tgz#3db471f45aae4a994a0688322171f51b8b91bee5" @@ -3558,6 +3566,18 @@ sha.js@^2.3.6: dependencies: inherits "^2.0.1" +showdown-highlightjs-extension@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/showdown-highlightjs-extension/-/showdown-highlightjs-extension-0.1.2.tgz#0fc90190283c1ae03fc4cccce3f1be6a5a58e4ce" + dependencies: + highlightjs "^9.8.0" + +showdown@^1.6.4: + version "1.6.4" + resolved "https://registry.yarnpkg.com/showdown/-/showdown-1.6.4.tgz#056bbb654ecdb8d8643ae12d6d597893ccaf46c6" + dependencies: + yargs "^6.6.0" + signal-exit@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -4347,7 +4367,7 @@ yargs@^4.7.1: y18n "^3.2.1" yargs-parser "^2.4.1" -yargs@^6.0.0: +yargs@^6.0.0, yargs@^6.6.0: version "6.6.0" resolved "https://registry.yarnpkg.com/yargs/-/yargs-6.6.0.tgz#782ec21ef403345f830a808ca3d513af56065208" dependencies: