├── .babelrc.js
├── .browserslistrc
├── .circleci
└── config.yml
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .stylelintignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── bower.json
├── dist
├── marky-marked.css
├── marky-marked.js
├── marky-marked.min.js
└── marky-marked.umd.js
├── images
├── marky-marked-dialog.png
├── marky-marked-fullscreen.png
└── marky-marked.png
├── index.html
├── karma.conf.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── rollup.config.base.js
├── rollup.config.min.js
├── rollup.config.test.js
├── rollup.config.umd.js
├── src
├── Marky.js
├── Store.js
├── elements
│ ├── Button.js
│ ├── Dialog.js
│ ├── Element.js
│ ├── HeadingDialog.js
│ ├── HeadingItem.js
│ ├── Icon.js
│ ├── ImageDialog.js
│ ├── LinkDialog.js
│ └── Separator.js
├── index.js
├── initializer.js
└── utils
│ ├── markdownHandlers.js
│ └── parsers.js
├── stylelint.config.js
├── styles
├── marky-dialogs.css
├── marky-editor.css
├── marky-marked.css
├── marky-toolbar.css
└── variables.css
└── test
├── Marky.spec.js
├── Store.spec.js
├── buttons.spec.js
├── dialogs.spec.js
├── elements
├── Button.spec.js
├── Dialog.spec.js
├── Element.spec.js
├── HeadingDialog.spec.js
├── HeadingItem.spec.js
├── Icon.spec.js
├── ImageDialog.spec.js
├── LinkDialog.spec.js
└── Separator.spec.js
├── headings.spec.js
├── initializer.spec.js
├── markymark.spec.js
└── utils
├── block-handler.spec.js
├── indent-handler.spec.js
├── inline-handler.spec.js
├── insert-handler.spec.js
├── list-handler.spec.js
└── parsers.spec.js
/.babelrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "presets": [
3 | [
4 | "env",
5 | {
6 | "modules": false
7 | }
8 | ]
9 | ],
10 | "plugins": [ "transform-object-rest-spread", "external-helpers", "array-includes" ]
11 | }
12 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | defaults
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | jobs:
3 | test:
4 | docker:
5 | - image: circleci/node:8-browsers
6 | steps:
7 | - checkout
8 | - run: node -v
9 | - run: npm -v
10 | - run:
11 | name: Install dependencies
12 | command: npm install
13 | - run:
14 | name: Run unit tests
15 | command: npm test
16 | - run:
17 | name: Generate code coverage
18 | command: cat ./coverage/**/lcov.info | ./node_modules/codecov/bin/codecov
19 | - store_artifacts:
20 | path: coverage
21 | prefix: coverage
22 | - store_test_results:
23 | path: tmp/karma-results
24 | deploy:
25 | docker:
26 | - image: circleci/node:8
27 | steps:
28 | - checkout
29 | - run:
30 | name: Set git configs
31 | command: |
32 | git config credential.helper 'cache --timeout=120'
33 | git config user.email "patrick.fricano@icloud.com"
34 | git config user.name "CircleCi"
35 | - run:
36 | name: Checkout gh-pages branch
37 | command: |
38 | git fetch
39 | git checkout gh-pages
40 | git rebase master
41 | - run:
42 | name: Push to gh-pages branch
43 | command: git push origin gh-pages
44 | workflows:
45 | version: 2
46 | test_and_deploy:
47 | jobs:
48 | - test
49 | - deploy:
50 | requires:
51 | - test
52 | filters:
53 | branches:
54 | only: master
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist
2 | vendor
3 | node_modules
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: 'babel-eslint',
3 | extends: ['airbnb-base'],
4 | env: {
5 | browser: true
6 | },
7 | plugins: ['import'],
8 | rules: {
9 | 'import/no-extraneous-dependencies': [
10 | 'error',
11 | {
12 | devDependencies: ['**/*.spec.js', '**/*conf*.js']
13 | }
14 | ]
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | npm-debug.log
4 | yarn-error.log
5 | coverage
6 | .vscode
7 | tmp
8 | vendor
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | README.md
2 | index.html
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | #### 4.0
2 |
3 | More breaking changes! Yay! This update actually includes some big changes.
4 |
5 | The biggest change is now the library exports just a function, `markymark`. Rather than `marky.mark()` you will just use `markymark()` to initialize it.
6 |
7 | In moving further away from the DOM for the public API, I've incorporated [contra/emitter](https://github.com/bevacqua/contra#λemitterthing-options) so that now the Marky object itself emits the public events ('markychange', 'markyupdate', etc.). No more having to register CustomEvents to be able to interface with the events.
8 |
9 | This package also now uses Rollup for the build. This resulted in significantly a smaller library, even with the addition of the emitter. About 20KB was shaved off the minified file.
10 |
11 | Tests are now written with Tape instead of Mocha/Chai, as well.
12 |
13 | #### 3.0
14 |
15 | Another breaking change was introduced. Instead of writing the html to a hidden input in the DOM it is now written to the marky object in the `html` prop. Similarly, the current state's markdown is written to the `markdown` prop.
16 |
17 | Finally, fullscreen and the `fullscreen-toggled` class have been renamed. The concept of "fullscreen" is now called "expanded view" since it may only fill its container depending on your styles and layout. The class is now `marky-expanded`.
18 |
19 | #### 2.0
20 |
21 | Not much has really changed, but there is a breaking change in that now `mark` should have elements directly passed in, rather than tag names. To migrate you really only need to switch your function call from `mark('funky-bunch')` to `mark(document.getElementsByTagName('funcky-bunch'))`.
22 |
23 | It accepts an array of elemtents, HTMLCollection, and NodeList. You cannot pass in an element directly; even if it's one element just wrap it in an array.
24 |
25 | `marky-mark` elements still are initialized by default.
26 |
27 | This change should make it a little more flexible and a little easier to work with in frameworks like React and Vue so you can now just pass in refs within your components.
28 |
29 | #### v1.5
30 |
31 | - Under the hood: Switched to standardjs
32 | - New `destroy()` method for removing editors from the DOM if need be.
33 |
34 | #### v1.4
35 |
36 | State management is a lot smarter now. Instead of the previous behavior where a markyupdate event would fire on every input event (meaning, every time a character is added or removed), and undo/redos would just go back or forward 5 state, updates are now fired on the following events:
37 |
38 | - period input
39 | - comma input
40 | - question mark input
41 | - exclamation point input
42 | - colon input
43 | - semi-colon input
44 | - back slash input
45 | - forward slash input
46 | - ampersand input
47 | - vertical pipe input
48 | - space input (but not a space directly following any of the above punctuation or another space)
49 | - Deletion of a bulk selection (using the delete key)
50 | - Any toolbar button is used (aside from undo, redo, and fullscreen)
51 | - The editor's value is committed by uthe user, meaning focus has moved off of the editor
52 | - Lastly, if there's ever more than a one-second pause
53 |
54 | This essentially makes all toolbar functionality push an update, but also any word input and deliberate deletion of more than one character.
55 |
56 | The main reasoning behind this change is to make the editor more performant. Rather than constantly writing state with every little change Marky Marked can be a bit more selective. I haven't run any benchmarks but by watching stats on my computer I noticed a drastic reduction in processor power needing to be used.
57 |
58 | The other added benefit is that now we have a lot more state that can be written, since we're writing state a lot less frequently.
59 |
60 | #### v1.3.5
61 |
62 | - Mostly clean up. Only potentially breaking change should be that I've changed the super generic id that's added to most elements from `editor-0` for instance to `marky-mark-0` (as an id for the container element, as a class for everything else).
63 |
64 | #### v1.3.4
65 |
66 | - Resizing is now turned off by default for the textarea in the stylesheet. This can be overridden, of course.
67 |
68 | #### v1.3
69 |
70 | - Fullscreen. Hitting the new fullscreen button in the toolbar will toggle `fullscreen-toggled` classes on the container as well as the editor. With this you can make the Marky Marked editor fill the entire browser window. Of course, the included stylesheet already handles it all for you, if you're using it.
71 | - Link and image insertion now behaves a bit differently. If any text is selected in the editor this text will be autopopulated into the alt text or display text input in the dialog. Additionally, instead of always inserting the Markdown snippet after the selected text, Marky Marked will now replace the selected text with the snippet. Which makes more sense when allowing for the autopopulation.
72 |
73 | #### v1.2
74 |
75 | - Headings selection is no longer done in a `` element, but is instead a dialog with a ``. This was done to make the experience easier to style and make it consistent between browsers.
76 | - The update event has been replaced with the more consistently named "markyupdate" event.
77 | - Some work with accessibility.
78 | - Bug fixes.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015 Patrick Fricano
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Marky Marked
2 |
3 | _A `
"+h(e.message+"",!0)+" ";throw e}}b.exec=b,p.options=p.setOptions=function(e){return y(p.defaults,e),p},p.getDefaults=function(){return{baseUrl:null,breaks:!1,gfm:!0,headerIds:!0,headerPrefix:"",highlight:null,langPrefix:"language-",mangle:!0,pedantic:!1,renderer:new o,sanitize:!1,sanitizer:null,silent:!1,smartLists:!1,smartypants:!1,tables:!0,xhtml:!1}},p.defaults=p.getDefaults(),p.Parser=u,p.parser=u.parse,p.Renderer=o,p.TextRenderer=l,p.Lexer=s,p.lexer=s.lex,p.InlineLexer=i,p.inlineLexer=i.output,p.parse=p,e.exports=p}(b||"undefined"!=typeof window&&window)}(y={exports:{}},y.exports),y.exports);class w{constructor(e=[]){this.timeline=e,this.index=0}get state(){return this.timeline[this.index]}get html(){return this.state.html}get markdown(){return this.state.markdown}get selection(){return this.state.selection}push(e){this.timeline=this.timeline.slice(0,this.index+1),this.timeline.push(e),this.index+=1,this.index>999&&(this.timeline.shift(),this.index-=1)}update(e,t){const n=x(e,{sanitize:!0}).toString()||"";this.push({markdown:e,html:n,selection:t})}undo(e){this.index=this.index>e-1?this.index-e:0}redo(e){this.index=this.index{const l=0===t,u=1===t;s.lastIndexOf(n,e)===e-o&&(s=s.substring(0,e-o)+s.substring(e),l&&(r[0]-=o,r[1]-=o),u&&!i[0]&&(r[1]-=n.length),i[t]=""),s.indexOf(n,e)===e&&(s=s.substring(0,e)+s.substring(e+o),l&&(r[0]!==r[1]&&(r[1]-=o),r[0]===r[1]&&(r[0]-=o)),u&&i[0]&&(r[1]+=o),i[t]="")}),{value:s.substring(0,r[0])+i[0]+s.substring(r[0],r[1])+i[1]+s.substring(r[1]),range:[r[0]+i[0].length,r[1]+i[1].length]}}function S(e,t,n){const s=t[0],r=t[1],i=/[0-9~*`_-]|\b|\n|$/gm;let o,l=v(e,s),u=_(e,r);if(p(e,/^[#>]/m,l)===l){const t=e.substring(l).search(i),s=e.substring(l,t);return o=e.substring(0,l)+e.substring(t,e.length),u-=s.length,n.trim().length&&s.trim()!==n.trim()&&(o=e.substring(0,l)+n+e.substring(t,e.length),l+=n.length,u+=n.length),{value:o,range:[l,u]}}return{value:o=e.substring(0,l)+n+e.substring(l,e.length),range:[s+n.length,r+n.length]}}function $(e,t,n){const s=v(e,t[0]),r=_(e,t[1]),i=k(e.substring(s,r)),o=/[~*`_[!]|[a-zA-Z]|\r|\n|$/gm,l=[];i.forEach((e,t)=>{const s="ul"===n?"- ":`${t+1}. `;let r;if(0===p(e,/^[0-9#>-]/m,0)){const t=e.substring(0,0+e.substring(0).search(o));return r=e.substring(e.search(o),e.length),t.trim()!==s.trim()&&(r=s+e.substring(e.search(o),e.length)),l.push(r)}return r=s+e.substring(0,e.length),l.push(r)});const u=l.join("\r\n");return{value:e.substring(0,s)+l.join("\r\n")+e.substring(r,e.length),range:[s,s+u.replace(/\n/gm,"").length]}}function C(e,t,n){const s=v(e,t[0]),r=_(e,t[1]),i=k(e.substring(s,r)),o=[];i.forEach(e=>{let t;return"out"===n?(t=0===e.indexOf(" ",0)?e.substring(" ".length,e.length):e.substring(e.search(/[~*`_[!#>-]|[a-zA-Z0-9]|\r|\n|$/gm),e.length),o.push(t)):(t=" "+e.substring(0,e.length),o.push(t))});const l=o.join("\r\n");return{value:e.substring(0,s)+o.join("\r\n")+e.substring(r,e.length),range:[s,s+l.replace(/\n/gm,"").length]}}function A(e,t,n){const s=t[0],r=t[1];return{value:e.substring(0,s)+n+e.substring(r,e.length),range:[s,s+n.length]}}class T{constructor(e,t,n){return this.id=e,this.editor=n.element,this.container=t,this.store=new w([{markdown:"",html:"",selection:[0,0]}]),this.elements={dialogs:{},buttons:{},editor:n},this}get state(){return this.store.state}get html(){return this.state.html}get markdown(){return this.state.markdown}get selection(){return this.state.selection}destroy(e=this.container){this.removeListeners(this.elements),this.elements={dialogs:{},buttons:{},editor:null},this.editor=null,this.container=null,e.parentNode&&e.parentNode.removeChild(e)}update(e,t=[0,0]){return this.store.update(e,t),this.emit("markychange"),this.store.index}undo(e=1,t=this.editor){return this.store.undo(e),this.updateEditor(this.store.state.markdown,this.store.state.selection,t),this.emit("markychange"),this.store.index}redo(e=1,t=this.editor){return this.store.redo(e),this.updateEditor(this.store.state.markdown,this.store.state.selection,t),this.emit("markychange"),this.store.index}setSelection(e=[0,0],t=this.editor){return t.setSelectionRange(e[0],e[1]),e}expandSelectionForward(e=0,t=this.editor){const n=t.selectionStart,s=t.selectionEnd+e;return t.setSelectionRange(n,s),[n,s]}expandSelectionBackward(e=0,t=this.editor){const n=t.selectionStart-e,s=t.selectionEnd;return t.setSelectionRange(n,s),[n,s]}moveCursorBackward(e=0,t=this.editor){const n=t.selectionStart-e;return t.setSelectionRange(n,n),n}moveCursorForward(e=0,t=this.editor){const n=t.selectionStart+e;return t.setSelectionRange(n,n),n}bold(e=[this.editor.selectionStart,this.editor.selectionEnd],t=this.editor){const n=E(t.value,e,"**");return this.updateEditor(n.value,n.range,t),this.emit("markyupdate"),[n.range[0],n.range[1]]}italic(e=[this.editor.selectionStart,this.editor.selectionEnd],t=this.editor){const n=E(t.value,e,"_");return this.updateEditor(n.value,n.range,t),this.emit("markyupdate"),[n.range[0],n.range[1]]}strikethrough(e=[this.editor.selectionStart,this.editor.selectionEnd],t=this.editor){const n=E(t.value,e,"~~");return this.updateEditor(n.value,n.range,t),this.emit("markyupdate"),[n.range[0],n.range[1]]}code(e=[this.editor.selectionStart,this.editor.selectionEnd],t=this.editor){const n=E(t.value,e,"`");return this.updateEditor(n.value,n.range,t),this.emit("markyupdate"),[n.range[0],n.range[1]]}blockquote(e=[this.editor.selectionStart,this.editor.selectionEnd],t=this.editor){const n=S(t.value,e,"> ");return this.updateEditor(n.value,n.range,t),this.emit("markyupdate"),[n.range[0],n.range[1]]}heading(e=0,t=[this.editor.selectionStart,this.editor.selectionEnd],n=this.editor){const s=[];for(let t=1;t<=e;t+=1)s.push("#");const r=s.join(""),i=r?" ":"",o=S(n.value,t,r+i);return this.updateEditor(o.value,o.range,n),this.emit("markyupdate"),[o.range[0],o.range[1]]}link(e=[this.editor.selectionStart,this.editor.selectionEnd],t="http://url.com",n="http://url.com",s=this.editor){const r=`[${n}](${t})`,i=A(s.value,e,r);return this.updateEditor(i.value,i.range,s),this.emit("markyupdate"),[i.range[0],i.range[1]]}image(e=[this.editor.selectionStart,this.editor.selectionEnd],t="http://imagesource.com/image.jpg",n="http://imagesource.com/image.jpg",s=this.editor){const r=``,i=A(s.value,e,r);return this.updateEditor(i.value,i.range,s),this.emit("markyupdate"),[i.range[0],i.range[1]]}unorderedList(e=[this.editor.selectionStart,this.editor.selectionEnd],t=this.editor){const n=$(t.value,e,"ul");return this.updateEditor(n.value,n.range,t),this.emit("markyupdate"),[n.range[0],n.range[1]]}orderedList(e=[this.editor.selectionStart,this.editor.selectionEnd],t=this.editor){const n=$(t.value,e,"ol");return this.updateEditor(n.value,n.range,t),this.emit("markyupdate"),[n.range[0],n.range[1]]}indent(e=[this.editor.selectionStart,this.editor.selectionEnd],t=this.editor){const n=C(t.value,e,"in");return this.updateEditor(n.value,n.range,t),this.emit("markyupdate"),[n.range[0],n.range[1]]}outdent(e=[this.editor.selectionStart,this.editor.selectionEnd],t=this.editor){const n=C(t.value,e,"out");return this.updateEditor(n.value,n.range,t),this.emit("markyupdate"),[n.range[0],n.range[1]]}updateEditor(e,t,n=this.editor){n.value=e,n.setSelectionRange(t[0],t[1])}removeListeners(e){Object.values(e).forEach(e=>{e.removeListeners?e.removeListeners():this.removeListeners(e)})}}class L{constructor(e,t={}){this.type=e,this.element=this.create(),this.listeners={},Object.entries(t).forEach(([e,t])=>{this.assign(e,t)})}create(){return document.createElement(this.type)}assign(e,t){return this.element[e]=t,this}appendTo(e){return e.appendChild(this.element),this}appendToElement(e){return e.element.appendChild(this.element),this}appendElements(e){return e.forEach(e=>this.element.appendChild(e.element)),this}addClass(...e){return this.element.classList.add(...e.map(e=>e.replace(/[ ]/g,"-").toLowerCase())),this}removeClass(...e){return this.element.classList.remove(...e.map(e=>e.replace(/[ ]/g,"-").toLowerCase())),this}toggleClass(...e){return this.element.classList.toggle(...e.map(e=>e.replace(/[ ]/g,"-").toLowerCase())),this}listen(e,t){return this.listeners[e]=t,this.element.addEventListener(e,t),this}removeListener(e,t){return this.element.removeEventListener(e,t),this}removeListeners(){return Object.keys(this.listeners).forEach(e=>{this.removeListener(e,this.listeners[e])}),this}}class R extends L{constructor(...e){super("i"),this.addClass(...e)}}class z extends L{constructor(e,t,...n){super("button",{title:t,value:t,type:"button"}),this.addClass(t,e),this.icon=new R(...n).appendToElement(this)}}class O extends L{constructor(e,t){super("div",{title:t}),this.addClass(e,t,"dialog")}}class Z extends O{constructor(e){super(e,"Link Dialog"),this.form=new L("form",{id:`${e}-link-form`,title:"Link Form"}).appendToElement(this),this.urlInput=new L("input",{type:"text",name:`${e}-link-url-input`,placeholder:"http://url.com",title:"Link Url"}).addClass("link-url-input"),this.nameInput=new L("input",{type:"text",name:`${e}-link-display-input`,placeholder:"Display text",title:"Link Display"}).addClass("link-display-input"),this.insertButton=new L("button",{type:"submit",textContent:"Insert",title:"Insert Link"}).addClass("insert-link"),this.form.appendElements([this.urlInput,this.nameInput,this.insertButton])}}class j extends O{constructor(e){super(e,"Image Dialog"),this.form=new L("form",{id:`${e}-image-form`,title:"Image Form"}).appendToElement(this),this.urlInput=new L("input",{type:"text",name:`${e}-image-source-input`,placeholder:"http://url.com/image.jpg",title:"Image Source"}).addClass("image-source-input"),this.nameInput=new L("input",{type:"text",name:`${e}-image-alt-input`,placeholder:"Alt text",title:"Image Alt"}).addClass("image-alt-input"),this.insertButton=new L("button",{type:"submit",textContent:"Insert",title:"Insert Image"}).addClass("insert-image"),this.form.appendElements([this.urlInput,this.nameInput,this.insertButton])}}class q extends L{constructor(e,t,...n){super("li",{title:e,value:t}),this.addClass(e),this.button=new L("button",{type:"button",value:t}).addClass("heading-button",e).appendToElement(this),n.length?this.icon=new R(...n).appendToElement(this.button):this.button.assign("textContent",t)}}class M extends O{constructor(e){super(e,"Heading Dialog"),this.headingList=new L("ul",{title:"Heading List"}).addClass(`${e}-heading-list`).appendToElement(this),this.options=[...Array(6)].map((e,t)=>new q(`Heading ${t+1}`,t+1)),this.options.push(new q("Remove Heading",0,"fa","fa-remove")),this.options.forEach(e=>{e.appendToElement(this.headingList)})}}class B extends L{constructor(){super("span"),this.addClass("separator")}}var D=e=>{if(!(e instanceof HTMLElement))throw new TypeError("argument should be an HTMLElement");if(e.children.length)return null;const t=`marky-mark-${m()}`;e.id=t;const n=new L("div",{title:"Toolbar"}).addClass("marky-toolbar",t),s=new L("div",{title:"Dialogs"}).addClass("marky-dialogs",t),r=new L("textarea",{title:"Marky Marked Editor"}).addClass("marky-editor",t),i=g(new T(t,e,r));function o(e){e.preventDefault(),r.element.focus()}let l;e.marky=i,i.elements.dialogs={heading:new M(t),link:new Z(t),image:new j(t)},Object.entries(i.elements.dialogs).forEach(([e,t])=>{"heading"===e?t.options.forEach(e=>{e.listen("click",e=>{e.preventDefault();const n=parseInt(e.target.value,10);r.element.focus(),t.removeClass("toggled"),i.heading(n,[r.element.selectionStart,r.element.selectionEnd])})}):t.form.listen("submit",n=>{n.preventDefault(),r.element.focus();const s=t.urlInput.element.value.slice(0)||"http://url.com",o=t.nameInput.element.value.slice(0)||s;t.removeClass("toggled"),i[e]([r.element.selectionStart,r.element.selectionEnd],s,o)})}),i.elements.buttons={heading:new z(t,"Heading","fa","fa-header").addClass("marky-border-left","marky-border-right"),bold:new z(t,"Bold","fa","fa-bold").addClass("marky-border-left"),italic:new z(t,"Italic","fa","fa-italic"),strikethrough:new z(t,"Strikethrough","fa","fa-strikethrough"),code:new z(t,"Code","fa","fa-code"),blockquote:new z(t,"Blockquote","fa","fa-quote-right").addClass("marky-border-right"),link:new z(t,"Link","fa","fa-link").addClass("marky-border-left"),image:new z(t,"Image","fa","fa-file-image-o").addClass("marky-border-right"),unorderedList:new z(t,"Unordered List","fa","fa-list-ul").addClass("marky-border-left"),orderedList:new z(t,"Ordered List","fa","fa-list-ol"),outdent:new z(t,"Outdent","fa","fa-outdent"),indent:new z(t,"Indent","fa","fa-indent").addClass("marky-border-right"),undo:new z(t,"Undo","fa","fa-backward").addClass("marky-border-left"),redo:new z(t,"Redo","fa","fa-forward").addClass("marky-border-right"),expand:new z(t,"Expand","fa","fa-expand").addClass("marky-border-left","marky-border-right")},Object.entries(i.elements.buttons).forEach(([t,n])=>{n.listen("mousedown",o),Object.keys(i.elements.dialogs).includes(t)?(n.dialog=i.elements.dialogs[t].element,n.listen("click",()=>{Object.keys(i.elements.dialogs).forEach(e=>{e===t?i.elements.dialogs[t].toggleClass("toggled"):i.elements.dialogs[e].removeClass("toggled")}),"link"!==t&&"image"!==t||!n.dialog.classList.contains("toggled")||(n.dialog.children[0].children[1].value=r.element.value.substring(r.element.selectionStart,r.element.selectionEnd))})):"expand"===t?n.listen("click",()=>{e.classList.toggle("marky-expanded"),n.toggleClass("marky-expanded"),r.toggleClass("marky-expanded"),n.icon.toggleClass("fa-expand"),n.icon.toggleClass("fa-compress")}):n.listen("click",e=>(function(e,t){e.classList.contains("disabled")||(["undo","redo"].includes(t)?i[t]():i[t]([r.element.selectionStart,r.element.selectionEnd]))})(e.currentTarget,t))}),n.appendTo(e),r.appendTo(e),n.appendElements([i.elements.buttons.heading,new B,i.elements.buttons.bold,i.elements.buttons.italic,i.elements.buttons.strikethrough,i.elements.buttons.code,i.elements.buttons.blockquote,new B,i.elements.buttons.link,i.elements.buttons.image,new B,i.elements.buttons.unorderedList,i.elements.buttons.orderedList,i.elements.buttons.outdent,i.elements.buttons.indent,new B,i.elements.buttons.undo,i.elements.buttons.redo,new B,i.elements.buttons.expand,s]),s.appendElements([i.elements.dialogs.link,i.elements.dialogs.image,i.elements.dialogs.heading]);let u=0;const h=[],a=[46,44,63,33,58,59,47,92,38,124,32];return r.listen("input",()=>{window.clearTimeout(l),l=window.setTimeout(()=>{i.emit("markyupdate")},1e3)}),r.listen("change",()=>{i.emit("markyupdate")}),r.listen("paste",()=>{setTimeout(()=>{i.emit("markyupdate")},0)}),r.listen("cut",()=>{setTimeout(()=>{i.emit("markyupdate")},0)}),r.listen("keydown",e=>{8===e.which&&(u=e.currentTarget.selectionEnd-e.currentTarget.selectionStart)}),r.listen("keypress",e=>{h.push(e.which),h.length>2&&h.shift(),a.forEach(t=>32===e.which&&h[0]===t?window.clearTimeout(l):e.which===t?(window.clearTimeout(l),i.emit("markyupdate")):void 0)}),r.listen("keyup",e=>{8===e.which&&u>0&&(window.clearTimeout(l),u=0,i.emit("markyupdate"))}),r.listen("click",()=>{i.elements.dialogs.image.removeClass("toggled"),i.elements.dialogs.link.removeClass("toggled"),i.elements.dialogs.heading.removeClass("toggled")}),r.listen("select",()=>i.emit("markyselect")),r.listen("blur",()=>i.emit("markyblur")),r.listen("focus",()=>i.emit("markyfocus")),i.on("markyupdate",()=>{i.update(r.element.value,[r.element.selectionStart,r.element.selectionEnd])}),i.on("markychange",()=>{0===i.store.index?i.elements.buttons.undo.addClass("disabled"):i.elements.buttons.undo.removeClass("disabled"),i.store.index===i.store.timeline.length-1?i.elements.buttons.redo.addClass("disabled"):i.elements.buttons.redo.removeClass("disabled")}),i};return function(e=document.getElementsByTagName("marky-mark")){if(e instanceof HTMLElement)return D(e);if(!(e instanceof Array||e instanceof HTMLCollection||e instanceof NodeList))throw new TypeError("argument should be an HTMLElement, Array, HTMLCollection");return Array.from(e).map(D)}});
2 |
--------------------------------------------------------------------------------
/images/marky-marked-dialog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/patrickfatrick/marky-marked/333a7e78d1a67a20f17b856d4cb06ef4be0932bc/images/marky-marked-dialog.png
--------------------------------------------------------------------------------
/images/marky-marked-fullscreen.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/patrickfatrick/marky-marked/333a7e78d1a67a20f17b856d4cb06ef4be0932bc/images/marky-marked-fullscreen.png
--------------------------------------------------------------------------------
/images/marky-marked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/patrickfatrick/marky-marked/333a7e78d1a67a20f17b856d4cb06ef4be0932bc/images/marky-marked.png
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Marky Marked
5 |
6 |
7 |
8 |
9 |
10 |
11 |
109 |
110 |
111 |
112 |
113 |
Marky Marked
114 |
115 |
116 |
117 |
Markdown => HTML
118 |
119 |
120 |
121 |
122 |
HTML => Rendered page
123 |
124 |
125 |
126 |
127 |
145 |
146 |
147 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | const summary = require('tap-summary');
2 | const rollupConfig = require('./rollup.config.test.js');
3 |
4 | module.exports = (karma) => {
5 | karma.set({
6 | basePath: '',
7 | files: [
8 | 'vendor/tape.js',
9 | 'vendor/sinon.js',
10 | 'src/**/*.js',
11 | 'test/**/*.spec.js',
12 | ],
13 | frameworks: ['tap'],
14 | rollupPreprocessor: rollupConfig,
15 | browsers: ['ChromeHeadless'],
16 | client: { captureConsole: false },
17 | preprocessors: {
18 | 'src/**/*.js': ['rollup'],
19 | 'test/**/*.spec.js': ['rollup'],
20 | },
21 | reporters: ['tap-pretty', 'coverage', 'junit'],
22 | coverageReporter: {
23 | reporters: [
24 | {
25 | type: 'lcov',
26 | dir: 'coverage',
27 | },
28 | ],
29 | },
30 | tapReporter: {
31 | prettify: summary,
32 | },
33 | junitReporter: {
34 | outputDir: 'tmp/karma-results',
35 | },
36 | logLevel: karma.LOG_INFO,
37 | singleRun: true,
38 | autoWatch: false,
39 | browserNoActivityTimeout: 30000,
40 | colors: true,
41 | loggers: [{ type: 'console' }],
42 | });
43 | };
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "marky-marked",
3 | "version": "5.0.3",
4 | "description": "A so sweet you'll be feeling good vibrations.",
5 | "main": "dist/marky-marked.umd.js",
6 | "module": "dist/marky-marked.js",
7 | "format": "es6",
8 | "engines": {
9 | "node": "^8.0.0"
10 | },
11 | "scripts": {
12 | "test": "npm run lint:js:test && karma start",
13 | "build": "concurrently npm:lint:* && concurrently npm:build:*",
14 | "lint:js:test": "eslint ./test/**/*.spec.js",
15 | "lint:js:src": "eslint ./src/**/*.js",
16 | "lint:css:src": "stylelint styles/**/*.css -f string",
17 | "lint-fix": "esw --fix --cache -w src test",
18 | "build:base": "rollup -c 'rollup.config.base.js'",
19 | "build:umd": "rollup -c 'rollup.config.umd.js'",
20 | "build:min": "rollup -c 'rollup.config.min.js'",
21 | "build:styles": "postcss 'styles/marky-marked.css' -c -o 'dist/marky-marked.css'",
22 | "vendor:tape": "browserify node_modules/tape-catch/index.js --standalone test -o vendor/tape.js",
23 | "vendor:sinon": "browserify node_modules/sinon/lib/sinon.js --standalone sinon -o vendor/sinon.js",
24 | "postinstall": "rm -rf vendor; mkdir -p vendor; concurrently npm:vendor:*"
25 | },
26 | "repository": {
27 | "type": "git",
28 | "url": "https://www.github.com/patrickfatrick/marky-marked"
29 | },
30 | "keywords": [
31 | "Javascript",
32 | "textarea",
33 | "Markdown",
34 | "content editor",
35 | "WYSIWYG"
36 | ],
37 | "author": "Patrick Fricano ",
38 | "license": "MIT",
39 | "devDependencies": {
40 | "babel-core": "^6.24.1",
41 | "babel-eslint": "^10.0.1",
42 | "babel-plugin-array-includes": "^2.0.3",
43 | "babel-plugin-external-helpers": "^6.22.0",
44 | "babel-plugin-istanbul": "^5.1.0",
45 | "babel-plugin-transform-object-rest-spread": "^6.23.0",
46 | "babel-preset-env": "^1.3.3",
47 | "browserify": "^14.3.0",
48 | "codecov": "^3.1.0",
49 | "concurrently": "^4.1.0",
50 | "cssnano": "^4.1.8",
51 | "eslint": "^5.11.0",
52 | "eslint-config-airbnb-base": "^13.1.0",
53 | "eslint-plugin-import": "^2.14.0",
54 | "eslint-watch": "^4.0.2",
55 | "faucet": "^0.0.1",
56 | "font-awesome": "^4.5.0",
57 | "karma": "^1.7.1",
58 | "karma-chrome-launcher": "^2.2.0",
59 | "karma-coverage": "^1.1.1",
60 | "karma-junit-reporter": "^1.2.0",
61 | "karma-rollup-preprocessor": "^6.1.1",
62 | "karma-sourcemap-loader": "^0.3.7",
63 | "karma-tap": "^4.1.4",
64 | "karma-tap-pretty-reporter": "^4.1.0",
65 | "postcss-cli": "^6.1.0",
66 | "postcss-cssnext": "^3.1.0",
67 | "postcss-import": "^12.0.1",
68 | "postcss-preset-env": "^6.5.0",
69 | "rollup": "^0.68.2",
70 | "rollup-plugin-babel": "^3.0.7",
71 | "rollup-plugin-commonjs": "^9.2.0",
72 | "rollup-plugin-node-resolve": "^4.0.0",
73 | "rollup-plugin-terser": "^3.0.0",
74 | "sinon": "^7.2.2",
75 | "stylelint": "^9.9.0",
76 | "stylelint-config-standard": "^18.2.0",
77 | "stylelint-order": "^2.0.0",
78 | "tap-summary": "^4.0.0",
79 | "tape": "^4.6.3",
80 | "tape-catch": "^1.0.6"
81 | },
82 | "dependencies": {
83 | "contra": "^1.9.4",
84 | "harsh": "^1.5.1",
85 | "marked": "^0.5.2"
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | const postcssImport = require('postcss-import');
2 | const postcssPresetEnv = require('postcss-preset-env');
3 | const cssnano = require('cssnano');
4 |
5 | module.exports = {
6 | plugins: [
7 | postcssImport,
8 | postcssPresetEnv({
9 | stage: 0,
10 | }),
11 | cssnano({
12 | autoprefixer: false,
13 | }),
14 | ],
15 | };
16 |
--------------------------------------------------------------------------------
/rollup.config.base.js:
--------------------------------------------------------------------------------
1 | import resolve from 'rollup-plugin-node-resolve';
2 | import babel from 'rollup-plugin-babel';
3 | import commonjs from 'rollup-plugin-commonjs';
4 |
5 | export default {
6 | input: 'src/index.js',
7 | plugins: [
8 | commonjs(),
9 | resolve(),
10 | babel({
11 | exclude: 'node_modules/**/*',
12 | }),
13 | ],
14 | output: {
15 | format: 'es',
16 | file: 'dist/marky-marked.js',
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/rollup.config.min.js:
--------------------------------------------------------------------------------
1 | import { terser } from 'rollup-plugin-terser';
2 | import base from './rollup.config.base';
3 |
4 | export default Object.assign(base, {
5 | output: {
6 | file: 'dist/marky-marked.min.js',
7 | format: 'umd',
8 | name: 'markymark',
9 | },
10 | plugins: base.plugins.concat(terser()),
11 | });
12 |
--------------------------------------------------------------------------------
/rollup.config.test.js:
--------------------------------------------------------------------------------
1 | const resolve = require('rollup-plugin-node-resolve');
2 | const babel = require('rollup-plugin-babel');
3 | const commonjs = require('rollup-plugin-commonjs');
4 |
5 | // Have to use CommonJS for tests,
6 | // and build it from scratch rather than using the base config
7 | module.exports = {
8 | input: 'src/index.js',
9 | plugins: [
10 | commonjs({
11 | namedExports: {
12 | 'node_modules/harsh/dist/harsh.js': ['hashish'],
13 | },
14 | }),
15 | resolve(),
16 | babel({
17 | exclude: 'node_modules/**/*',
18 | plugins: ['transform-object-rest-spread', 'external-helpers', 'array-includes', 'istanbul'],
19 | }),
20 | ],
21 | output: {
22 | format: 'iife',
23 | globals: {
24 | tape: 'test',
25 | sinon: 'sinon',
26 | },
27 | name: 'markymark',
28 | sourceMap: 'inline',
29 | },
30 | external: ['tape', 'sinon'],
31 | };
32 |
--------------------------------------------------------------------------------
/rollup.config.umd.js:
--------------------------------------------------------------------------------
1 | import base from './rollup.config.base';
2 |
3 | export default Object.assign(base, {
4 | output: {
5 | file: 'dist/marky-marked.umd.js',
6 | format: 'umd',
7 | name: 'markymark',
8 | },
9 | });
10 |
--------------------------------------------------------------------------------
/src/Marky.js:
--------------------------------------------------------------------------------
1 | import Store from './Store';
2 | import {
3 | inlineHandler, blockHandler, insertHandler, listHandler, indentHandler,
4 | } from './utils/markdownHandlers';
5 |
6 | export default class Marky {
7 | constructor(id, container, editor) {
8 | this.id = id;
9 | this.editor = editor.element;
10 | this.container = container;
11 | this.store = new Store([
12 | {
13 | markdown: '',
14 | html: '',
15 | selection: [0, 0],
16 | },
17 | ]);
18 | this.elements = {
19 | dialogs: {},
20 | buttons: {},
21 | editor,
22 | };
23 |
24 | return this;
25 | }
26 |
27 | get state() {
28 | return this.store.state;
29 | }
30 |
31 | get html() {
32 | return this.state.html;
33 | }
34 |
35 | get markdown() {
36 | return this.state.markdown;
37 | }
38 |
39 | get selection() {
40 | return this.state.selection;
41 | }
42 |
43 | /**
44 | * Removes the container and all descendants from the DOM
45 | * @param {container} container the container used to invoke `mark()`
46 | */
47 | destroy(container = this.container) {
48 | // Remove all listeners from all elements
49 | this.removeListeners(this.elements);
50 |
51 | // Reset elements contained in this instance to remove from memory
52 | this.elements = {
53 | dialogs: {},
54 | buttons: {},
55 | editor: null,
56 | };
57 | this.editor = null;
58 | this.container = null;
59 |
60 | if (container.parentNode) {
61 | container.parentNode.removeChild(container);
62 | }
63 | }
64 |
65 | /**
66 | * Handles the `markyupdate` event
67 | * @param {String} markdown the new markdown blob
68 | * @param {Number[]} selection selectionStart and selectionEnd indices
69 | */
70 | update(markdown, selection = [0, 0]) {
71 | this.store.update(markdown, selection);
72 | this.emit('markychange');
73 | return this.store.index;
74 | }
75 |
76 | /**
77 | * Handles moving backward in state
78 | * @param {Number} num number of states to move back
79 | * @param {HTMLElement} editor the marky marked editor
80 | * @returns {Number} the new index
81 | */
82 | undo(num = 1, editor = this.editor) {
83 | this.store.undo(num);
84 | this.updateEditor(this.store.state.markdown, this.store.state.selection, editor);
85 | this.emit('markychange');
86 | return this.store.index;
87 | }
88 |
89 | /**
90 | * Handles moving forward in state
91 | * @param {Number} num number of states to move forward
92 | * @param {HTMLElement} editor the marky marked editor
93 | * @returns {Number} the new index
94 | */
95 | redo(num = 1, editor = this.editor) {
96 | this.store.redo(num);
97 | this.updateEditor(this.store.state.markdown, this.store.state.selection, editor);
98 | this.emit('markychange');
99 | return this.store.index;
100 | }
101 |
102 | /**
103 | * Sets the selection indices in the editor
104 | * @param {Number[]} arr starting and ending indices
105 | * @param {HTMLElement} editor the marky marked editor
106 | * @returns {Number[]} the array that was passed in
107 | */
108 | setSelection(arr = [0, 0], editor = this.editor) {
109 | editor.setSelectionRange(arr[0], arr[1]);
110 | return arr;
111 | }
112 |
113 | /**
114 | * expands the selection to the right
115 | * @param {Number} num number of characters to expand by
116 | * @param {HTMLElement} editor the marky marked editor
117 | * @returns {Number[]} the new selection indices
118 | */
119 | expandSelectionForward(num = 0, editor = this.editor) {
120 | const start = editor.selectionStart;
121 | const end = editor.selectionEnd + num;
122 |
123 | editor.setSelectionRange(start, end);
124 | return [start, end];
125 | }
126 |
127 | /**
128 | * expands the selection to the left
129 | * @param {Number} num number of characters to expand by
130 | * @param {HTMLElement} editor the marky marked editor
131 | * @returns {Number[]} the new selection indices
132 | */
133 | expandSelectionBackward(num = 0, editor = this.editor) {
134 | const start = editor.selectionStart - num;
135 | const end = editor.selectionEnd;
136 |
137 | editor.setSelectionRange(start, end);
138 | return [start, end];
139 | }
140 |
141 | /**
142 | * expands the cursor to the right
143 | * @param {Number} num number of characters to move by
144 | * @param {HTMLElement} editor the marky marked editor
145 | * @returns {Number} the new cursor position
146 | */
147 | moveCursorBackward(num = 0, editor = this.editor) {
148 | const start = editor.selectionStart - num;
149 |
150 | editor.setSelectionRange(start, start);
151 | return start;
152 | }
153 |
154 | /**
155 | * expands the cursor to the left
156 | * @param {Number} num number of characters to move by
157 | * @param {HTMLElement} editor the marky marked editor
158 | * @returns {Number} the new cursor position
159 | */
160 | moveCursorForward(num = 0, editor = this.editor) {
161 | const start = editor.selectionStart + num;
162 |
163 | editor.setSelectionRange(start, start);
164 | return start;
165 | }
166 |
167 | /**
168 | * implements a bold on a selection
169 | * @requires handlers/inlineHandler
170 | * @param {Number[]} indices starting and ending positions for the selection
171 | * @param {HTMLElement} editor the marky marked editor
172 | * @returns {Number[]} the new selection after the bold
173 | */
174 | bold(indices = [this.editor.selectionStart, this.editor.selectionEnd], editor = this.editor) {
175 | const boldify = inlineHandler(editor.value, indices, '**');
176 | this.updateEditor(boldify.value, boldify.range, editor);
177 | this.emit('markyupdate');
178 | return [boldify.range[0], boldify.range[1]];
179 | }
180 |
181 | /**
182 | * implements an italic on a selection
183 | * @requires handlers/inlineHandler
184 | * @param {Number[]} indices starting and ending positions for the selection
185 | * @param {HTMLElement} editor the marky marked editor
186 | * @returns {Number[]} the new selection after the italic
187 | */
188 | italic(indices = [this.editor.selectionStart, this.editor.selectionEnd], editor = this.editor) {
189 | const italicize = inlineHandler(editor.value, indices, '_');
190 | this.updateEditor(italicize.value, italicize.range, editor);
191 | this.emit('markyupdate');
192 | return [italicize.range[0], italicize.range[1]];
193 | }
194 |
195 | /**
196 | * implements a strikethrough on a selection
197 | * @requires handlers/inlineHandler
198 | * @param {Number[]} indices starting and ending positions for the selection
199 | * @param {HTMLElement} editor the marky marked editor
200 | * @returns {Number[]} the new selection after the strikethrough
201 | */
202 | strikethrough(
203 | indices = [this.editor.selectionStart, this.editor.selectionEnd],
204 | editor = this.editor,
205 | ) {
206 | const strikitize = inlineHandler(editor.value, indices, '~~');
207 | this.updateEditor(strikitize.value, strikitize.range, editor);
208 | this.emit('markyupdate');
209 | return [strikitize.range[0], strikitize.range[1]];
210 | }
211 |
212 | /**
213 | * implements a code on a selection
214 | * @requires handlers/inlineHandler
215 | * @param {Number[]} indices starting and ending positions for the selection
216 | * @param {HTMLElement} editor the marky marked editor
217 | * @returns {Number[]} the new selection after the code
218 | */
219 | code(indices = [this.editor.selectionStart, this.editor.selectionEnd], editor = this.editor) {
220 | const codify = inlineHandler(editor.value, indices, '`');
221 | this.updateEditor(codify.value, codify.range, editor);
222 | this.emit('markyupdate');
223 | return [codify.range[0], codify.range[1]];
224 | }
225 |
226 | /**
227 | * implements a blockquote on a selection
228 | * @requires handlers/blockHandler
229 | * @param {Number[]} indices starting and ending positions for the selection
230 | * @param {HTMLElement} editor the marky marked editor
231 | * @returns {Number[]} the new selection after the bold
232 | */
233 | blockquote(
234 | indices = [this.editor.selectionStart, this.editor.selectionEnd],
235 | editor = this.editor,
236 | ) {
237 | const quotify = blockHandler(editor.value, indices, '> ');
238 | this.updateEditor(quotify.value, quotify.range, editor);
239 | this.emit('markyupdate');
240 | return [quotify.range[0], quotify.range[1]];
241 | }
242 |
243 | /**
244 | * implements a heading on a selection
245 | * @requires handlers/blockHandler
246 | * @param {Number[]} indices starting and ending positions for the selection
247 | * @param {HTMLElement} editor the marky marked editor
248 | * @returns {Number[]} the new selection after the heading
249 | */
250 | heading(
251 | value = 0,
252 | indices = [this.editor.selectionStart, this.editor.selectionEnd],
253 | editor = this.editor,
254 | ) {
255 | const markArr = [];
256 |
257 | for (let i = 1; i <= value; i += 1) {
258 | markArr.push('#');
259 | }
260 | const mark = markArr.join('');
261 | const space = mark ? ' ' : '';
262 | const headingify = blockHandler(editor.value, indices, mark + space);
263 | this.updateEditor(headingify.value, headingify.range, editor);
264 | this.emit('markyupdate');
265 | return [headingify.range[0], headingify.range[1]];
266 | }
267 |
268 | /**
269 | * inserts a link snippet at the end of a selection
270 | * @requires handlers/insertHandler
271 | * @param {Number[]} indices starting and ending positions for the selection
272 | * @param {HTMLElement} editor the marky marked editor
273 | * @returns {Number[]} the new selection after the snippet is inserted
274 | */
275 | link(
276 | indices = [this.editor.selectionStart, this.editor.selectionEnd],
277 | url = 'http://url.com',
278 | display = 'http://url.com',
279 | editor = this.editor,
280 | ) {
281 | const mark = `[${display}](${url})`;
282 | const linkify = insertHandler(editor.value, indices, mark);
283 | this.updateEditor(linkify.value, linkify.range, editor);
284 | this.emit('markyupdate');
285 | return [linkify.range[0], linkify.range[1]];
286 | }
287 |
288 | /**
289 | * inserts an image snippet at the end of a selection
290 | * @requires handlers/insertHandler
291 | * @param {Number[]} indices starting and ending positions for the selection
292 | * @param {HTMLElement} editor the marky marked editor
293 | * @returns {Number[]} the new selection after the snippet is inserted
294 | */
295 | image(
296 | indices = [this.editor.selectionStart, this.editor.selectionEnd],
297 | source = 'http://imagesource.com/image.jpg',
298 | alt = 'http://imagesource.com/image.jpg',
299 | editor = this.editor,
300 | ) {
301 | const mark = ``;
302 | const imageify = insertHandler(editor.value, indices, mark);
303 | this.updateEditor(imageify.value, imageify.range, editor);
304 | this.emit('markyupdate');
305 | return [imageify.range[0], imageify.range[1]];
306 | }
307 |
308 | /**
309 | * implements an unordered list on a selection
310 | * @requires handlers/listHandler
311 | * @param {Number[]} indices starting and ending positions for the selection
312 | * @param {HTMLElement} editor the marky marked editor
313 | * @returns {Number[]} the new selection after the list is implemented
314 | */
315 | unorderedList(
316 | indices = [this.editor.selectionStart, this.editor.selectionEnd],
317 | editor = this.editor,
318 | ) {
319 | const listify = listHandler(editor.value, indices, 'ul');
320 | this.updateEditor(listify.value, listify.range, editor);
321 | this.emit('markyupdate');
322 | return [listify.range[0], listify.range[1]];
323 | }
324 |
325 | /**
326 | * implements an ordered list on a selection
327 | * @requires handlers/listHandler
328 | * @param {Number[]} indices starting and ending positions for the selection
329 | * @param {HTMLElement} editor the marky marked editor
330 | * @returns {Number[]} the new selection after the list is implemented
331 | */
332 | orderedList(
333 | indices = [this.editor.selectionStart, this.editor.selectionEnd],
334 | editor = this.editor,
335 | ) {
336 | const listify = listHandler(editor.value, indices, 'ol');
337 | this.updateEditor(listify.value, listify.range, editor);
338 | this.emit('markyupdate');
339 | return [listify.range[0], listify.range[1]];
340 | }
341 |
342 | /**
343 | * implements an indent on a selection
344 | * @requires handlers/indentHandler
345 | * @param {Number[]} indices starting and ending positions for the selection
346 | * @param {HTMLElement} editor the marky marked editor
347 | * @returns {Number[]} the new selection after the indent is implemented
348 | */
349 | indent(indices = [this.editor.selectionStart, this.editor.selectionEnd], editor = this.editor) {
350 | const indentify = indentHandler(editor.value, indices, 'in');
351 | this.updateEditor(indentify.value, indentify.range, editor);
352 | this.emit('markyupdate');
353 | return [indentify.range[0], indentify.range[1]];
354 | }
355 |
356 | /**
357 | * implements an outdent on a selection
358 | * @requires handlers/indentHandler
359 | * @param {Number[]} indices starting and ending positions for the selection
360 | * @param {HTMLElement} editor the marky marked editor
361 | * @returns {Number[]} the new selection after the outdent is implemented
362 | */
363 | outdent(indices = [this.editor.selectionStart, this.editor.selectionEnd], editor = this.editor) {
364 | const indentify = indentHandler(editor.value, indices, 'out');
365 | this.updateEditor(indentify.value, indentify.range, editor);
366 | this.emit('markyupdate');
367 | return [indentify.range[0], indentify.range[1]];
368 | }
369 |
370 | /**
371 | * @private
372 | * Handles updating the editor's value and selection range
373 | * @param {Object} handled value = string; range = start and end of selection
374 | * @param {HTMLElement} editor the marky marked editor
375 | */
376 | updateEditor(markdown, range, editor = this.editor) {
377 | editor.value = markdown; // eslint-disable-line no-param-reassign
378 | editor.setSelectionRange(range[0], range[1]);
379 | }
380 |
381 | /**
382 | * @private
383 | * Rescursively searches an object for elements and removes their listeners
384 | * @param {Object} obj plain object (used only with the `elements` object in Marky)
385 | */
386 | removeListeners(obj) {
387 | Object.values(obj).forEach((value) => {
388 | if (value.removeListeners) {
389 | value.removeListeners();
390 | } else {
391 | this.removeListeners(value);
392 | }
393 | });
394 | }
395 | }
396 |
--------------------------------------------------------------------------------
/src/Store.js:
--------------------------------------------------------------------------------
1 | import marked from 'marked';
2 |
3 | export default class Store {
4 | constructor(timeline = []) {
5 | this.timeline = timeline;
6 | this.index = 0;
7 | }
8 |
9 | get state() {
10 | return this.timeline[this.index];
11 | }
12 |
13 | get html() {
14 | return this.state.html;
15 | }
16 |
17 | get markdown() {
18 | return this.state.markdown;
19 | }
20 |
21 | get selection() {
22 | return this.state.selection;
23 | }
24 |
25 | /**
26 | * Handles adding and removing state
27 | * @param {Object} newState the new state to push
28 | */
29 | push(newState) {
30 | this.timeline = this.timeline.slice(0, this.index + 1);
31 | this.timeline.push(newState);
32 | this.index += 1;
33 | if (this.index > 999) {
34 | this.timeline.shift();
35 | this.index -= 1;
36 | }
37 | }
38 |
39 | /**
40 | * updates the state
41 | * @external marked
42 | * @param {String} markdown markdown blob
43 | * @param {Number[]} selection selectionStart and selectionEnd indices
44 | */
45 | update(markdown, selection) {
46 | const html = marked(markdown, { sanitize: true }).toString() || '';
47 |
48 | this.push({ markdown, html, selection });
49 | }
50 |
51 | /**
52 | * moves backward in state
53 | * @param {Number} num the number of states to move back by
54 | */
55 | undo(num) {
56 | this.index = (this.index > (num - 1))
57 | ? this.index - num
58 | : 0;
59 | }
60 |
61 | /**
62 | * moves forwardin state
63 | * @param {Number} num the number of states to move forward by
64 | */
65 | redo(num) {
66 | this.index = (this.index < this.timeline.length - (num + 1))
67 | ? this.index + num
68 | : this.timeline.length - 1;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/elements/Button.js:
--------------------------------------------------------------------------------
1 | import Element from './Element';
2 | import Icon from './Icon';
3 |
4 | /**
5 | * Creates HTML button elements
6 | * @type {Element}
7 | * @requires Icon
8 | * @param {String} title title for the element
9 | * @param {String} id editor ID to associate with the element
10 | * @param {String[]} iconClasses classes to use for elements
11 | */
12 | export default class Button extends Element {
13 | constructor(id, title, ...iconClasses) {
14 | super('button', { title, value: title, type: 'button' });
15 | this.addClass(title, id);
16 |
17 | this.icon = new Icon(...iconClasses)
18 | .appendToElement(this);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/elements/Dialog.js:
--------------------------------------------------------------------------------
1 | import Element from './Element';
2 |
3 | /**
4 | * Creates dialog elements
5 | * @type {Element}
6 | * @param {String} title title for the element
7 | * @param {String} id editor ID to associate with the element
8 | */
9 | export default class Dialog extends Element {
10 | constructor(id, title) {
11 | super('div', { title });
12 | this.addClass(id, title, 'dialog');
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/elements/Element.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Creates an HTML element with some built-in shortcut methods
3 | * @param {String} type tag name for the element
4 | * @param {String} id editor ID to associate with the element
5 | * @param {Object} props props to assign to the element
6 | */
7 | export default class Element {
8 | constructor(type, props = {}) {
9 | this.type = type;
10 | this.element = this.create();
11 | this.listeners = {};
12 | Object.entries(props).forEach(([prop, value]) => {
13 | this.assign(prop, value);
14 | });
15 | }
16 |
17 | create() {
18 | return document.createElement(this.type);
19 | }
20 |
21 | assign(prop, value) {
22 | this.element[prop] = value;
23 |
24 | return this;
25 | }
26 |
27 | appendTo(container) {
28 | container.appendChild(this.element);
29 |
30 | return this;
31 | }
32 |
33 | /**
34 | * @param {Element} element an instance of Element
35 | */
36 | appendToElement(element) {
37 | element.element.appendChild(this.element);
38 |
39 | return this;
40 | }
41 |
42 | /**
43 | * @param {Element[]} elements an array of elements
44 | */
45 | appendElements(elements) {
46 | elements.forEach(element => this.element.appendChild(element.element));
47 |
48 | return this;
49 | }
50 |
51 | addClass(...classNames) {
52 | this.element.classList.add(
53 | ...classNames.map(name => name.replace(/[ ]/g, '-').toLowerCase()),
54 | );
55 |
56 | return this;
57 | }
58 |
59 | removeClass(...classNames) {
60 | this.element.classList.remove(
61 | ...classNames.map(name => name.replace(/[ ]/g, '-').toLowerCase()),
62 | );
63 |
64 | return this;
65 | }
66 |
67 | toggleClass(...classNames) {
68 | this.element.classList.toggle(
69 | ...classNames.map(name => name.replace(/[ ]/g, '-').toLowerCase()),
70 | );
71 |
72 | return this;
73 | }
74 |
75 | listen(evt, cb) {
76 | this.listeners[evt] = cb;
77 | this.element.addEventListener(evt, cb);
78 |
79 | return this;
80 | }
81 |
82 | removeListener(evt, cb) {
83 | this.element.removeEventListener(evt, cb);
84 |
85 | return this;
86 | }
87 |
88 | removeListeners() {
89 | Object.keys(this.listeners).forEach((listener) => {
90 | this.removeListener(listener, this.listeners[listener]);
91 | });
92 |
93 | return this;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/elements/HeadingDialog.js:
--------------------------------------------------------------------------------
1 | import Element from './Element';
2 | import Dialog from './Dialog';
3 | import HeadingItem from './HeadingItem';
4 |
5 | /**
6 | * Creates heading dialog element
7 | * @type {Dialog}
8 | * @param {String} id editor ID to associate with the element
9 | * @param {String} title title for the element
10 | */
11 | export default class HeadingDialog extends Dialog {
12 | constructor(id) {
13 | super(id, 'Heading Dialog');
14 |
15 | this.headingList = new Element('ul', { title: 'Heading List' })
16 | .addClass(`${id}-heading-list`)
17 | .appendToElement(this);
18 |
19 | this.options = [...Array(6)]
20 | .map((_, i) => new HeadingItem(`Heading ${i + 1}`, i + 1));
21 | this.options.push(new HeadingItem('Remove Heading', 0, 'fa', 'fa-remove'));
22 |
23 | this.options.forEach((option) => {
24 | option.appendToElement(this.headingList);
25 | });
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/elements/HeadingItem.js:
--------------------------------------------------------------------------------
1 | import Element from './Element';
2 | import Icon from './Icon';
3 |
4 | /**
5 | * Creates HTML option elements
6 | * @type {Element}
7 | * @requires Icon
8 | * @param {String} title title for the element
9 | * @param {String} value a value to assign the element
10 | * @param {Array} iconClasses classes to use for elements
11 | */
12 | export default class HeadingItem extends Element {
13 | constructor(title, value, ...iconClasses) {
14 | super('li', { title, value });
15 | this.addClass(title);
16 |
17 | this.button = new Element('button', { type: 'button', value })
18 | .addClass('heading-button', title)
19 | .appendToElement(this);
20 |
21 | if (iconClasses.length) {
22 | this.icon = new Icon(...iconClasses)
23 | .appendToElement(this.button);
24 | } else {
25 | this.button.assign('textContent', value);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/elements/Icon.js:
--------------------------------------------------------------------------------
1 | import Element from './Element';
2 |
3 | /**
4 | * Creates HTML i elements
5 | * @type {Element}
6 | * @param {String[]} classNames classes to use with element
7 | */
8 | export default class Icon extends Element {
9 | constructor(...classNames) {
10 | super('i');
11 | this.addClass(...classNames);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/elements/ImageDialog.js:
--------------------------------------------------------------------------------
1 | import Element from './Element';
2 | import Dialog from './Dialog';
3 |
4 | /**
5 | * Creates image dialog modal
6 | * @type {Dialog}
7 | * @param {String} title title for the element
8 | * @param {String} id editor ID to associate with the element
9 | */
10 | export default class ImageDialog extends Dialog {
11 | constructor(id) {
12 | super(id, 'Image Dialog');
13 |
14 | this.form = new Element('form', { id: `${id}-image-form`, title: 'Image Form' })
15 | .appendToElement(this);
16 |
17 | this.urlInput = new Element('input', {
18 | type: 'text',
19 | name: `${id}-image-source-input`,
20 | placeholder: 'http://url.com/image.jpg',
21 | title: 'Image Source',
22 | })
23 | .addClass('image-source-input');
24 |
25 | this.nameInput = new Element('input', {
26 | type: 'text',
27 | name: `${id}-image-alt-input`,
28 | placeholder: 'Alt text',
29 | title: 'Image Alt',
30 | })
31 | .addClass('image-alt-input');
32 |
33 | this.insertButton = new Element('button', {
34 | type: 'submit',
35 | textContent: 'Insert',
36 | title: 'Insert Image',
37 | })
38 | .addClass('insert-image');
39 |
40 | this.form.appendElements([
41 | this.urlInput,
42 | this.nameInput,
43 | this.insertButton,
44 | ]);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/elements/LinkDialog.js:
--------------------------------------------------------------------------------
1 | import Element from './Element';
2 | import Dialog from './Dialog';
3 |
4 | /**
5 | * Creates dialog (modal) elements
6 | * @type {Dialog}
7 | * @param {String} title title for the element
8 | * @param {String} id editor ID to associate with the element
9 | */
10 | export default class LinkDialog extends Dialog {
11 | constructor(id) {
12 | super(id, 'Link Dialog');
13 |
14 | this.form = new Element('form', {
15 | id: `${id}-link-form`,
16 | title: 'Link Form',
17 | })
18 | .appendToElement(this);
19 |
20 | this.urlInput = new Element('input', {
21 | type: 'text',
22 | name: `${id}-link-url-input`,
23 | placeholder: 'http://url.com',
24 | title: 'Link Url',
25 | })
26 | .addClass('link-url-input');
27 |
28 | this.nameInput = new Element('input', {
29 | type: 'text',
30 | name: `${id}-link-display-input`,
31 | placeholder: 'Display text',
32 | title: 'Link Display',
33 | })
34 | .addClass('link-display-input');
35 |
36 | this.insertButton = new Element('button', {
37 | type: 'submit',
38 | textContent: 'Insert',
39 | title: 'Insert Link',
40 | })
41 | .addClass('insert-link');
42 |
43 | this.form.appendElements([
44 | this.urlInput,
45 | this.nameInput,
46 | this.insertButton,
47 | ]);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/elements/Separator.js:
--------------------------------------------------------------------------------
1 | import Element from './Element';
2 |
3 | /**
4 | * Create separator spans for the toolbar
5 | * @type {Element}
6 | */
7 | export default class Separator extends Element {
8 | constructor() {
9 | super('span');
10 | this.addClass('separator');
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import initializer from './initializer';
2 |
3 | /**
4 | * Register and append the DOM elements needed and set the event listeners
5 | * @param {HTMLCollection} containers empty elements to initialize marky marked into
6 | */
7 | export default function markymark(container = document.getElementsByTagName('marky-mark')) {
8 | if (container instanceof HTMLElement) {
9 | return initializer(container);
10 | }
11 |
12 | if (
13 | !(container instanceof Array)
14 | && !(container instanceof HTMLCollection)
15 | && !(container instanceof NodeList)
16 | ) {
17 | throw new TypeError('argument should be an HTMLElement, Array, HTMLCollection');
18 | }
19 |
20 | return Array.from(container).map(initializer);
21 | }
22 |
--------------------------------------------------------------------------------
/src/initializer.js:
--------------------------------------------------------------------------------
1 | import { emitter } from 'contra';
2 | import { hashish } from 'harsh';
3 | import Marky from './Marky';
4 | import Element from './elements/Element';
5 | import Button from './elements/Button';
6 | import LinkDialog from './elements/LinkDialog';
7 | import ImageDialog from './elements/ImageDialog';
8 | import HeadingDialog from './elements/HeadingDialog';
9 | import Separator from './elements/Separator';
10 |
11 | /**
12 | * Register and append the DOM elements needed and set the event listeners
13 | * @param {HTMLElement} container empty element to initialize marky marked into
14 | */
15 | export default (container) => {
16 | if (!(container instanceof HTMLElement)) {
17 | throw new TypeError('argument should be an HTMLElement');
18 | }
19 |
20 | /**
21 | * Ignore container if it's not empty
22 | */
23 | if (container.children.length) {
24 | return null;
25 | }
26 |
27 | const id = `marky-mark-${hashish()}`;
28 | container.id = id; // eslint-disable-line no-param-reassign
29 |
30 | /**
31 | * Create and register main elements:
32 | * toolbar, editor, dialog container, hidden input
33 | */
34 |
35 | const toolbar = new Element('div', { title: 'Toolbar' }).addClass('marky-toolbar', id);
36 | const dialogs = new Element('div', { title: 'Dialogs' }).addClass('marky-dialogs', id);
37 | const markyEditor = new Element('textarea', { title: 'Marky Marked Editor' })
38 | .addClass('marky-editor', id);
39 |
40 | const marky = emitter(new Marky(id, container, markyEditor));
41 | container.marky = marky; // eslint-disable-line no-param-reassign
42 |
43 | /**
44 | * Create and register dialogs and set listeners
45 | */
46 |
47 | marky.elements.dialogs = {
48 | heading: new HeadingDialog(id),
49 | link: new LinkDialog(id),
50 | image: new ImageDialog(id),
51 | };
52 |
53 | Object.entries(marky.elements.dialogs).forEach(([dialogName, dialog]) => {
54 | if (dialogName === 'heading') {
55 | dialog.options.forEach((option) => {
56 | option.listen('click', (e) => {
57 | e.preventDefault();
58 | const value = parseInt(e.target.value, 10);
59 | markyEditor.element.focus();
60 | dialog.removeClass('toggled');
61 | marky.heading(
62 | value,
63 | [markyEditor.element.selectionStart, markyEditor.element.selectionEnd],
64 | );
65 | });
66 | });
67 | } else {
68 | dialog.form.listen('submit', (e) => {
69 | e.preventDefault();
70 | markyEditor.element.focus();
71 | const url = dialog.urlInput.element.value.slice(0) || 'http://url.com';
72 | const name = dialog.nameInput.element.value.slice(0) || url;
73 | dialog.removeClass('toggled');
74 | marky[dialogName](
75 | [markyEditor.element.selectionStart, markyEditor.element.selectionEnd],
76 | url,
77 | name,
78 | );
79 | });
80 | }
81 | });
82 |
83 | /**
84 | * Create and register toolbar buttons and set listeners
85 | */
86 |
87 | function buttonMousedown(e) {
88 | e.preventDefault();
89 | markyEditor.element.focus();
90 | }
91 |
92 | function buttonClick(button, name) {
93 | if (button.classList.contains('disabled')) return;
94 | if (['undo', 'redo'].includes(name)) {
95 | marky[name]();
96 | } else {
97 | marky[name]([markyEditor.element.selectionStart, markyEditor.element.selectionEnd]);
98 | }
99 | }
100 |
101 | marky.elements.buttons = {
102 | heading: new Button(id, 'Heading', 'fa', 'fa-header')
103 | .addClass('marky-border-left', 'marky-border-right'),
104 | bold: new Button(id, 'Bold', 'fa', 'fa-bold').addClass('marky-border-left'),
105 | italic: new Button(id, 'Italic', 'fa', 'fa-italic'),
106 | strikethrough: new Button(id, 'Strikethrough', 'fa', 'fa-strikethrough'),
107 | code: new Button(id, 'Code', 'fa', 'fa-code'),
108 | blockquote: new Button(id, 'Blockquote', 'fa', 'fa-quote-right').addClass('marky-border-right'),
109 | link: new Button(id, 'Link', 'fa', 'fa-link').addClass('marky-border-left'),
110 | image: new Button(id, 'Image', 'fa', 'fa-file-image-o').addClass('marky-border-right'),
111 | unorderedList: new Button(id, 'Unordered List', 'fa', 'fa-list-ul')
112 | .addClass('marky-border-left'),
113 | orderedList: new Button(id, 'Ordered List', 'fa', 'fa-list-ol'),
114 | outdent: new Button(id, 'Outdent', 'fa', 'fa-outdent'),
115 | indent: new Button(id, 'Indent', 'fa', 'fa-indent').addClass('marky-border-right'),
116 | undo: new Button(id, 'Undo', 'fa', 'fa-backward').addClass('marky-border-left'),
117 | redo: new Button(id, 'Redo', 'fa', 'fa-forward').addClass('marky-border-right'),
118 | expand: new Button(id, 'Expand', 'fa', 'fa-expand').addClass('marky-border-left', 'marky-border-right'),
119 | };
120 |
121 | Object.entries(marky.elements.buttons).forEach(([buttonName, button]) => {
122 | button.listen('mousedown', buttonMousedown);
123 | if (Object.keys(marky.elements.dialogs).includes(buttonName)) {
124 | // eslint-disable-next-line no-param-reassign
125 | button.dialog = marky.elements.dialogs[buttonName].element;
126 | button.listen('click', () => {
127 | Object.keys(marky.elements.dialogs).forEach((dialogName) => {
128 | if (dialogName === buttonName) marky.elements.dialogs[buttonName].toggleClass('toggled');
129 | else marky.elements.dialogs[dialogName].removeClass('toggled');
130 | });
131 |
132 | if (
133 | (buttonName === 'link'
134 | || buttonName === 'image')
135 | && button.dialog.classList.contains('toggled')
136 | ) {
137 | // eslint-disable-next-line no-param-reassign
138 | button.dialog.children[0].children[1].value = markyEditor.element.value.substring(
139 | markyEditor.element.selectionStart,
140 | markyEditor.element.selectionEnd,
141 | );
142 | }
143 | });
144 | } else if (buttonName === 'expand') {
145 | button.listen('click', () => {
146 | container.classList.toggle('marky-expanded');
147 | button.toggleClass('marky-expanded');
148 | markyEditor.toggleClass('marky-expanded');
149 | button.icon.toggleClass('fa-expand');
150 | button.icon.toggleClass('fa-compress');
151 | });
152 | } else {
153 | button.listen('click', e => buttonClick(e.currentTarget, buttonName));
154 | }
155 | });
156 |
157 | /**
158 | * Insert elements into the DOM one by one to ensure order
159 | */
160 |
161 | toolbar.appendTo(container);
162 | markyEditor.appendTo(container);
163 | toolbar.appendElements([
164 | marky.elements.buttons.heading,
165 | new Separator(),
166 | marky.elements.buttons.bold,
167 | marky.elements.buttons.italic,
168 | marky.elements.buttons.strikethrough,
169 | marky.elements.buttons.code,
170 | marky.elements.buttons.blockquote,
171 | new Separator(),
172 | marky.elements.buttons.link,
173 | marky.elements.buttons.image,
174 | new Separator(),
175 | marky.elements.buttons.unorderedList,
176 | marky.elements.buttons.orderedList,
177 | marky.elements.buttons.outdent,
178 | marky.elements.buttons.indent,
179 | new Separator(),
180 | marky.elements.buttons.undo,
181 | marky.elements.buttons.redo,
182 | new Separator(),
183 | marky.elements.buttons.expand,
184 | dialogs,
185 | ]);
186 | dialogs.appendElements([
187 | marky.elements.dialogs.link,
188 | marky.elements.dialogs.image,
189 | marky.elements.dialogs.heading,
190 | ]);
191 |
192 | /**
193 | * Listeners for the editor
194 | */
195 |
196 | let timeoutID; // Used input events
197 | let deleteSelection = 0; // Used for determing how to update state with deletions
198 | const keyMap = []; // Used for determining whether or not to update state on space keyup
199 | const punctuations = [
200 | 46, // period
201 | 44, // comma
202 | 63, // question mark
203 | 33, // exclamation point
204 | 58, // colon
205 | 59, // semi-colon
206 | 47, // back slash
207 | 92, // forward slash
208 | 38, // ampersand
209 | 124, // vertical pipe
210 | 32, // space
211 | ];
212 |
213 | /**
214 | * Listen for input events, set timeout to update state, clear timeout from previous input
215 | */
216 | markyEditor.listen('input', () => {
217 | window.clearTimeout(timeoutID);
218 | timeoutID = window.setTimeout(() => {
219 | marky.emit('markyupdate');
220 | }, 1000);
221 | });
222 |
223 | /**
224 | * Listen for change events (requires loss of focus) and update state
225 | */
226 | markyEditor.listen('change', () => {
227 | marky.emit('markyupdate');
228 | });
229 |
230 | /**
231 | * Listen for pasting into the editor and update state
232 | */
233 | markyEditor.listen('paste', () => {
234 | setTimeout(() => {
235 | marky.emit('markyupdate');
236 | }, 0);
237 | });
238 |
239 | /**
240 | * Listen for cutting from the editor and update state
241 | */
242 | markyEditor.listen('cut', () => {
243 | setTimeout(() => {
244 | marky.emit('markyupdate');
245 | }, 0);
246 | });
247 |
248 | /**
249 | * Listen for keydown events,
250 | * if key is delete key,
251 | * set deleteSelection to length of selection
252 | */
253 | markyEditor.listen('keydown', (e) => {
254 | if (e.which === 8) {
255 | deleteSelection = e.currentTarget.selectionEnd - e.currentTarget.selectionStart;
256 | }
257 | });
258 |
259 | /**
260 | * Listen for keyup events,
261 | * if key is space or punctuation (but not a space following punctuation or another space),
262 | * update state and clear input timeout.
263 | */
264 | markyEditor.listen('keypress', (e) => {
265 | keyMap.push(e.which);
266 | if (keyMap.length > 2) keyMap.shift();
267 | punctuations.forEach((punctuation) => { // eslint-disable-line consistent-return
268 | if (e.which === 32 && keyMap[0] === punctuation) {
269 | return window.clearTimeout(timeoutID);
270 | }
271 | if (e.which === punctuation) {
272 | window.clearTimeout(timeoutID);
273 | return marky.emit('markyupdate');
274 | }
275 | });
276 | });
277 |
278 | /**
279 | * Listen for keyup events,
280 | * if key is delete and it's a bulk selection,
281 | * update state and clear input timeout.
282 | */
283 | markyEditor.listen('keyup', (e) => {
284 | if (e.which === 8 && deleteSelection > 0) {
285 | window.clearTimeout(timeoutID);
286 | deleteSelection = 0;
287 | marky.emit('markyupdate');
288 | }
289 | });
290 |
291 | markyEditor.listen('click', () => {
292 | marky.elements.dialogs.image.removeClass('toggled');
293 | marky.elements.dialogs.link.removeClass('toggled');
294 | marky.elements.dialogs.heading.removeClass('toggled');
295 | });
296 |
297 | /**
298 | * The following just emit a marky event.
299 | */
300 | markyEditor.listen('select', () => marky.emit('markyselect'));
301 | markyEditor.listen('blur', () => marky.emit('markyblur'));
302 | markyEditor.listen('focus', () => marky.emit('markyfocus'));
303 |
304 | /**
305 | * Listeners for the marky instance
306 | */
307 |
308 | marky.on('markyupdate', () => {
309 | marky.update(
310 | markyEditor.element.value,
311 | [markyEditor.element.selectionStart, markyEditor.element.selectionEnd],
312 | );
313 | });
314 |
315 | marky.on('markychange', () => {
316 | if (marky.store.index === 0) {
317 | marky.elements.buttons.undo.addClass('disabled');
318 | } else {
319 | marky.elements.buttons.undo.removeClass('disabled');
320 | }
321 | if (marky.store.index === marky.store.timeline.length - 1) {
322 | marky.elements.buttons.redo.addClass('disabled');
323 | } else {
324 | marky.elements.buttons.redo.removeClass('disabled');
325 | }
326 | });
327 |
328 | return marky;
329 | };
330 |
--------------------------------------------------------------------------------
/src/utils/markdownHandlers.js:
--------------------------------------------------------------------------------
1 | import {
2 | indexOfMatch, splitLines, startOfLine, endOfLine,
3 | } from './parsers';
4 |
5 | /**
6 | * Handles wrapping format strings around a selection
7 | * @param {String} string the entire string
8 | * @param {Number[]} selectionRange the starting and ending positions of the selection
9 | * @param {String} symbol the format string to use
10 | * @returns {Object} the new string, the updated selectionRange
11 | */
12 | export function inlineHandler(string, selectionRange, symbol) {
13 | let newString = string;
14 | const newSelectionRange = [...selectionRange];
15 | // insertSymbols determines whether to add the symbol to either end of the selected text
16 | // Stat with assuming we will insert them (we will remove as necessary)
17 | const insertSymbols = [symbol, symbol];
18 | const symbolLength = symbol.length;
19 | const relevantPart = string
20 | .substring(selectionRange[0] - symbolLength, selectionRange[1] + symbolLength)
21 | .trim();
22 |
23 | // First check that the symbol is in the string at all
24 | if (relevantPart.includes(symbol)) {
25 | // If it is, for each index in the selection range...
26 | newSelectionRange.forEach((selectionIndex, j) => {
27 | const isStartingIndex = j === 0;
28 | const isEndingIndex = j === 1;
29 | // If the symbol immediately precedes the selection index...
30 | if (newString.lastIndexOf(symbol, selectionIndex) === selectionIndex - symbolLength) {
31 | // First trim it
32 | newString = newString.substring(0, selectionIndex - symbolLength)
33 | + newString.substring(selectionIndex);
34 |
35 | // Then adjust the selection range,
36 | // If this is the starting index in the range, we will have to adjust both
37 | // starting and ending indices
38 | if (isStartingIndex) {
39 | newSelectionRange[0] -= symbolLength;
40 | newSelectionRange[1] -= symbolLength;
41 | }
42 |
43 | if (isEndingIndex && !insertSymbols[0]) {
44 | newSelectionRange[1] -= symbol.length;
45 | }
46 |
47 | // Finally, disallow the symbol at this end of the selection
48 | insertSymbols[j] = '';
49 | }
50 |
51 | // If the symbol immediately follows the selection index...
52 | if (newString.indexOf(symbol, selectionIndex) === selectionIndex) {
53 | // Trim it
54 | newString = newString.substring(0, selectionIndex)
55 | + newString.substring(selectionIndex + symbolLength);
56 |
57 | // Then adjust the selection range,
58 | // If this is the starting index in the range...
59 | if (isStartingIndex) {
60 | // If the starting and ending indices are NOT the same (selection length > 0)
61 | // Adjust the ending selection down
62 | if (newSelectionRange[0] !== newSelectionRange[1]) {
63 | newSelectionRange[1] -= symbolLength;
64 | }
65 | // If the starting and ending indices are the same (selection length = 0)
66 | // Adjust the starting selection down
67 | if (newSelectionRange[0] === newSelectionRange[1]) {
68 | newSelectionRange[0] -= symbolLength;
69 | }
70 | }
71 |
72 | // If this is the ending index and the range
73 | // AND we're inserting the symbol at the starting index,
74 | // Adjust the ending selection up
75 | if (isEndingIndex && insertSymbols[0]) {
76 | newSelectionRange[1] += symbolLength;
77 | }
78 |
79 | // Finally, disallow the symbol at this end of the selection
80 | insertSymbols[j] = '';
81 | }
82 | });
83 | }
84 |
85 | // Put it all together
86 | const value = newString.substring(0, newSelectionRange[0])
87 | + insertSymbols[0]
88 | + newString.substring(newSelectionRange[0], newSelectionRange[1])
89 | + insertSymbols[1]
90 | + newString.substring(newSelectionRange[1]);
91 |
92 | return {
93 | value,
94 | range: [
95 | newSelectionRange[0] + insertSymbols[0].length,
96 | newSelectionRange[1] + insertSymbols[1].length,
97 | ],
98 | };
99 | }
100 |
101 | /**
102 | * Handles adding/removing a format string to a line
103 | * @param {String} string the entire string
104 | * @param {Number[]} selectionRange the starting and ending positions of the selection
105 | * @param {String} symbol the format string to use
106 | * @returns {Object} the new string, the updated indices
107 | */
108 | export function blockHandler(string, selectionRange, symbol) {
109 | const start = selectionRange[0];
110 | const end = selectionRange[1];
111 | const boundaryRegex = /[0-9~*`_-]|\b|\n|$/gm;
112 | let value;
113 | let lineStart = startOfLine(string, start);
114 | let lineEnd = endOfLine(string, end);
115 |
116 | // If there is a block handler symbol at the start of the line...
117 | if (indexOfMatch(string, /^[#>]/m, lineStart) === lineStart) {
118 | // Find the first boundary from the start of the formatting symbol
119 | // May include white space
120 | const existingSymbolBoundary = string.substring(lineStart).search(boundaryRegex);
121 | const existingSymbol = string.substring(
122 | lineStart,
123 | existingSymbolBoundary,
124 | );
125 |
126 | // Create new string without the existingSymbol
127 | value = string.substring(0, lineStart)
128 | + string.substring(
129 | existingSymbolBoundary,
130 | string.length,
131 | );
132 |
133 | // And also subtract the length of the symbol from the lineEnd index
134 | lineEnd -= existingSymbol.length;
135 |
136 | // If it's some other block handler...
137 | if (symbol.trim().length && existingSymbol.trim() !== symbol.trim()) {
138 | // Create a new string with the symbol inserted
139 | value = string.substring(0, lineStart)
140 | + symbol
141 | + string.substring(
142 | existingSymbolBoundary,
143 | string.length,
144 | );
145 | // And adjust lineStart and lineEnd indices
146 | lineStart += symbol.length;
147 | lineEnd += symbol.length;
148 | }
149 |
150 | return { value, range: [lineStart, lineEnd] };
151 | }
152 |
153 | // If not, pretty simple
154 | value = string.substring(0, lineStart) + symbol + string.substring(lineStart, string.length);
155 | return { value, range: [start + symbol.length, end + symbol.length] };
156 | }
157 |
158 | /**
159 | * Handles adding/removing format strings to groups of lines
160 | * @param {String} string the entire string to use
161 | * @param {Number[]} selectionRange the starting and ending positions of the selection
162 | * @param {String} type ul or ol
163 | * @returns {Object} the new string, the updated selectionRange
164 | */
165 | export function listHandler(string, selectionRange, type) {
166 | const start = startOfLine(string, selectionRange[0]);
167 | const end = endOfLine(string, selectionRange[1]);
168 | const lines = splitLines(string.substring(start, end));
169 | const boundaryRegex = /[~*`_[!]|[a-zA-Z]|\r|\n|$/gm;
170 | const newLines = [];
171 |
172 | lines.forEach((line, i) => {
173 | const symbol = (type === 'ul') ? '- ' : `${i + 1}. `;
174 | let newLine;
175 |
176 | // If the line begins with an existing list symbol
177 | if (indexOfMatch(line, /^[0-9#>-]/m, 0) === 0) {
178 | const existingSymbol = line.substring(
179 | 0,
180 | 0 + line.substring(0).search(boundaryRegex),
181 | );
182 |
183 | // Remove the symbol
184 | newLine = line.substring(line.search(boundaryRegex), line.length);
185 | if (existingSymbol.trim() !== symbol.trim()) {
186 | newLine = symbol + line.substring(line.search(boundaryRegex), line.length);
187 | }
188 | return newLines.push(newLine);
189 | }
190 | newLine = symbol + line.substring(0, line.length);
191 | return newLines.push(newLine);
192 | });
193 |
194 | // Put it all together
195 | const joined = newLines.join('\r\n');
196 | const value = string.substring(0, start) + newLines.join('\r\n') + string.substring(end, string.length);
197 | return { value, range: [start, start + joined.replace(/\n/gm, '').length] };
198 | }
199 |
200 | /**
201 | * Handles adding/removing indentation to groups of lines
202 | * @param {String} string the entire string to use
203 | * @param {Number[]} selectionRange the starting and ending positions to wrap
204 | * @param {String} type in or out
205 | * @returns {Object} the new string, the updated selectionRange
206 | */
207 | export function indentHandler(string, selectionRange, type) {
208 | const start = startOfLine(string, selectionRange[0]);
209 | const end = endOfLine(string, selectionRange[1]);
210 | const lines = splitLines(string.substring(start, end));
211 | const newLines = [];
212 |
213 | lines.forEach((line) => {
214 | const fourSpaces = ' ';
215 | let newLine;
216 | if (type === 'out') {
217 | newLine = (line.indexOf(fourSpaces, 0) === 0)
218 | ? line.substring(fourSpaces.length, line.length)
219 | : line.substring(line.search(/[~*`_[!#>-]|[a-zA-Z0-9]|\r|\n|$/gm), line.length);
220 | return newLines.push(newLine);
221 | }
222 | newLine = fourSpaces + line.substring(0, line.length);
223 | return newLines.push(newLine);
224 | });
225 |
226 | const joined = newLines.join('\r\n');
227 | const value = string.substring(0, start) + newLines.join('\r\n') + string.substring(end, string.length);
228 | return { value, range: [start, start + joined.replace(/\n/gm, '').length] };
229 | }
230 |
231 | /**
232 | * Handles inserting a snippet at the end of a selection
233 | * @param {String} string the entire string to use
234 | * @param {Number[]} selectionRange the starting and ending positions of the selection
235 | * @param {String} snippet the snippet to insert
236 | * @returns {Object} the new string, the updated selectionRange
237 | */
238 | export function insertHandler(string, selectionRange, snippet) {
239 | const start = selectionRange[0];
240 | const end = selectionRange[1];
241 | const value = string.substring(0, start) + snippet + string.substring(end, string.length);
242 |
243 | return { value, range: [start, start + snippet.length] };
244 | }
245 |
--------------------------------------------------------------------------------
/src/utils/parsers.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | /**
4 | * Finds the first index based on a regex match
5 | * @param {RegExp} regex a regex object
6 | * @param {Number} index optional starting index
7 | * @returns {Number} the index of the match
8 | */
9 | export function indexOfMatch(string, regex, index) {
10 | const str = (index !== null) ? string.substring(index) : string;
11 | const matches = str.match(regex);
12 | return matches ? str.indexOf(matches[0]) + index : -1;
13 | }
14 |
15 | /**
16 | * Finds the first index based on a regex match
17 | * @param {RegExp} regex a regex object
18 | * @param {Number} index optional starting index
19 | * @returns {Number} the index of the match
20 | */
21 | export function indicesOfMatches(string, regex, index) {
22 | const str = (index !== null) ? string.substring(index) : string;
23 | const matches = str.match(regex);
24 | const indices = [];
25 | matches.forEach((match, i) => {
26 | const prevIndex = indices ? indices[i - 1] : null;
27 | indices.push(str.indexOf(match, prevIndex + 1) + index);
28 | });
29 | return indices || -1;
30 | }
31 |
32 | /**
33 | * Finds the last index based on a regex match
34 | * @param {RegExp} regex a regex object
35 | * @param {Number} index optional ending index
36 | * @returns {Number} the index of the match
37 | */
38 | export function lastIndexOfMatch(string, regex, index) {
39 | const str = (index !== null) ? string.substring(0, index) : string;
40 | const matches = str.match(regex);
41 | return matches ? str.lastIndexOf(matches[matches.length - 1]) : -1;
42 | }
43 |
44 | /**
45 | * Creates an array of lines separated by line breaks
46 | * @param {Number} index optional ending index
47 | * @returns {String[]} an array of strings
48 | */
49 | export function splitLinesBackward(string, index) {
50 | const str = index ? string.substring(0, index) : string;
51 | return str.split(/\r\n|\r|\n/);
52 | }
53 |
54 | /**
55 | * Creates an array of lines split by line breaks
56 | * @param {Number} index optional starting index
57 | * @returns {String[]} an array of strings
58 | */
59 | export function splitLines(string, index) {
60 | const str = index ? string.substring(index) : string;
61 | return str.split(/\r\n|\r|\n/);
62 | }
63 |
64 | /**
65 | * Finds the start of a line
66 | * @param {Number} index optional position
67 | * @returns {Number} the index of the line start
68 | */
69 | export function startOfLine(string, index = 0) {
70 | return lastIndexOfMatch(string, /^.*/gm, index);
71 | }
72 |
73 | /**
74 | * Finds the end of a line
75 | * @param {Number} index optional position
76 | * @returns {Number} the index of the line end
77 | */
78 | export function endOfLine(string, index = 0) {
79 | return indexOfMatch(string, /(\r|\n|$)/gm, index);
80 | }
81 |
--------------------------------------------------------------------------------
/stylelint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: 'stylelint-config-standard',
3 | plugins: ['stylelint-order'],
4 | rules: {
5 | 'order/properties-alphabetical-order': true,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/styles/marky-dialogs.css:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .marky-dialogs {
4 | position: relative;
5 |
6 | & .dialog {
7 | background: var(--white);
8 | border: 1px solid var(--gray);
9 | box-shadow: var(--boxShadowA) 0 10px 15px, var(--boxShadowB) 0 5px 10px;
10 | margin: 0.7rem;
11 | margin-top: -50px;
12 | opacity: 0;
13 | padding: 1rem;
14 | position: absolute;
15 | transition: all 200ms ease-out;
16 | visibility: hidden;
17 |
18 | &.toggled {
19 | margin-top: 0.7rem;
20 | opacity: 1;
21 | visibility: visible;
22 | }
23 |
24 | &.heading-dialog {
25 | margin-left: 0;
26 | padding: 0;
27 |
28 | &.toggled {
29 | margin-top: -0.5rem;
30 | }
31 |
32 | & ul {
33 | list-style: none;
34 | margin: 0;
35 | padding: 0;
36 | text-align: center;
37 | width: 2.9rem;
38 | }
39 | }
40 |
41 | & input {
42 | border: none;
43 | border-bottom: 3px solid var(--gray);
44 | font-size: 1rem;
45 | margin-left: 0.2rem;
46 | margin-right: 0.2rem;
47 | outline: none;
48 | transition: all 200ms ease-out;
49 |
50 | &:focus {
51 | border-color: var(--deepBlue);
52 | }
53 | }
54 |
55 | & button {
56 | background-color: var(--deepBlue);
57 | border: 1px solid var(--gray);
58 | border-radius: 4px;
59 | color: var(--white);
60 | margin-bottom: 0;
61 | margin-left: 0.2rem;
62 | max-width: none;
63 |
64 | &:active {
65 | background: linear-gradient(var(--deepBlue) * 0.7, var(--deepBlue));
66 | color: var(--white) !important;
67 | }
68 |
69 | &:focus {
70 | background-color: var(--white);
71 | color: var(--lightGray);
72 | }
73 | }
74 |
75 | & .heading-button {
76 | background-color: var(--white);
77 | border: none;
78 | border-radius: 0;
79 | box-shadow: none;
80 | color: var(--lightGray);
81 | margin-left: 0;
82 | width: 2.9rem;
83 |
84 | &:hover,
85 | &:focus {
86 | background-color: var(--deepBlue);
87 | color: var(--white);
88 | }
89 |
90 | &:active {
91 | background: linear-gradient(var(--deepBlue) * 0.7, var(--deepBlue));
92 | color: var(--white) !important;
93 | }
94 |
95 | &[title="Remove Heading"]:hover,
96 | &[title="Remove Heading"]:focus {
97 | background-color: var(--fireTruck);
98 | color: var(--white);
99 | }
100 |
101 | &[title="Remove Heading"]:active {
102 | background: linear-gradient(var(--fireTruck) * 0.7, var(--fireTruck));
103 | color: var(--white) !important;
104 | }
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/styles/marky-editor.css:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .marky-editor {
4 | border: 2px solid var(--gray);
5 | font-size: 1rem;
6 | outline: none;
7 | padding: 8px 12px;
8 | resize: none;
9 | transition: border-color 200ms ease-out;
10 |
11 | &:focus {
12 | border-color: var(--deepBlue);
13 | }
14 |
15 | &.marky-expanded {
16 | box-shadow: var(--boxShadowA) 0 19px 60px, var(--boxShadowB) 0 15px 20px;
17 | height: 90% !important;
18 | resize: none;
19 | width: 100% !important;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/styles/marky-marked.css:
--------------------------------------------------------------------------------
1 | @import "variables";
2 | @import "marky-editor";
3 | @import "marky-toolbar";
4 | @import "marky-dialogs";
5 |
6 | [id^="marky-mark-"].marky-expanded {
7 | background: var(--white);
8 | box-sizing: border-box;
9 | height: 100%;
10 | left: 0;
11 | padding: 1rem;
12 | position: fixed;
13 | top: 0;
14 | width: 100%;
15 | z-index: 1000;
16 | }
17 |
--------------------------------------------------------------------------------
/styles/marky-toolbar.css:
--------------------------------------------------------------------------------
1 | @import "variables";
2 |
3 | .marky-toolbar {
4 | margin-bottom: 0.3rem;
5 | margin-left: 0.3rem;
6 | min-width: 100%;
7 | text-align: left;
8 |
9 | & button {
10 | background-color: var(--white);
11 | border: 1px solid var(--gray);
12 | border-left: none;
13 | border-radius: 0;
14 | color: var(--lightGray);
15 | cursor: pointer;
16 | font-size: 0.8rem;
17 | line-height: 0.8rem;
18 | margin-bottom: 0.3rem;
19 | margin-left: 0;
20 | margin-right: 0;
21 | max-width: 2rem;
22 | min-height: 1.5rem;
23 | min-width: 1.5rem;
24 | outline: none;
25 | padding: 6px;
26 | text-align: center;
27 | transition: box-shadow 200ms ease-out;
28 |
29 | &::-moz-focus-inner {
30 | border: 0;
31 | padding: 0;
32 | }
33 |
34 | &:hover {
35 | box-shadow: var(--boxShadowA) 0 1px 1px, var(--boxShadowB) 0 1px 5px;
36 | transition: unset;
37 | }
38 |
39 | &.active,
40 | &.disabled,
41 | &:active {
42 | background: linear-gradient(var(--white) * 0.7, var(--white));
43 | box-shadow: none;
44 | color: var(--gray) !important;
45 | }
46 |
47 | &.marky-border-right {
48 | border-bottom-right-radius: 4px;
49 | border-top-right-radius: 4px;
50 | }
51 |
52 | &.marky-border-left {
53 | border-bottom-left-radius: 4px;
54 | border-left: 1px solid var(--gray);
55 | border-top-left-radius: 4px;
56 | }
57 |
58 | &:focus:not(.disabled) {
59 | background-color: var(--deepBlue);
60 | color: var(--white);
61 | }
62 |
63 | @media (width >= 768px) {
64 | font-size: 1rem;
65 | }
66 | }
67 |
68 | & button.expand {
69 | &.marky-expanded {
70 | background-color: var(--orchid);
71 | color: var(--white);
72 |
73 | &:focus {
74 | background-color: var(--deepBlue);
75 | color: var(--white);
76 | }
77 |
78 | & .active,
79 | &:active {
80 | background: linear-gradient(var(--orchid) * 0.7, var(--orchid));
81 | color: var(--white) !important;
82 | }
83 | }
84 | }
85 |
86 | & button.heading {
87 | max-width: 3rem;
88 | padding-left: 1rem;
89 | padding-right: 1rem;
90 | width: 3rem;
91 | }
92 |
93 | & button.undo {
94 | padding-left: 4px;
95 | }
96 |
97 | & button.redo {
98 | padding-right: 4px;
99 | }
100 |
101 | & .separator {
102 | color: var(--gray);
103 | padding-left: 6px;
104 | padding-right: 6px;
105 |
106 | &:nth-child(11) {
107 | display: block;
108 |
109 | @media (width >= 600px) {
110 | display: inline;
111 | }
112 | }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/styles/variables.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --white: #fff;
3 | --gray: #919191;
4 | --lightGray: #666;
5 | --black: #000;
6 | --orchid: #da70d6;
7 | --deepBlue: #00b0ff;
8 | --fireTruck: #e84d49;
9 | --boxShadowA: rgba(0, 0, 0, 0.3);
10 | --boxShadowB: rgba(0, 0, 0, 0.22);
11 | }
12 |
--------------------------------------------------------------------------------
/test/Marky.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import sinon from 'sinon';
3 | import initializer from '../src/initializer';
4 | import Store from '../src/Store';
5 |
6 | const container = document.createElement('marky-mark');
7 | document.body.appendChild(container);
8 | const marky = initializer(container);
9 | const { editor } = marky;
10 |
11 | test('Marky > has an id', (t) => {
12 | t.true(marky.id.length > 0);
13 | t.end();
14 | });
15 |
16 | test('Marky > has a state', (t) => {
17 | t.true(marky.store instanceof Store);
18 | t.true(typeof marky.store.state === 'object');
19 | t.end();
20 | });
21 |
22 | test('Marky > has an index', (t) => {
23 | t.true(typeof marky.store.index === 'number');
24 | t.end();
25 | });
26 |
27 | test('Marky > destroy', (t) => {
28 | const newContainer = document.createElement('marky-mark');
29 | document.body.appendChild(newContainer);
30 | const newMarky = initializer(newContainer);
31 |
32 | t.ok(document.getElementById(newContainer.id));
33 | newMarky.destroy();
34 | t.notOk(document.getElementById(newContainer.id));
35 | t.end();
36 | });
37 |
38 | test('Marky > update', (t) => {
39 | sinon.spy(marky, 'emit');
40 | editor.value = 'Some text';
41 | const { length } = marky.store.timeline;
42 | marky.update(editor.value);
43 |
44 | t.equal(marky.store.timeline.length, length + 1);
45 | t.true(marky.emit.calledWith('markychange'));
46 | marky.emit.restore();
47 | t.end();
48 | });
49 |
50 | test('Marky > update is triggered by a markyupdate event', (t) => {
51 | sinon.spy(marky, 'update');
52 | marky.emit('markyupdate');
53 |
54 | t.true(marky.update.calledOnce);
55 | marky.update.restore();
56 | t.end();
57 | });
58 |
59 | test('Marky > undo', (t) => {
60 | sinon.spy(marky, 'emit');
61 | marky.store.index = 1;
62 | marky.undo(1);
63 |
64 | t.equal(marky.store.index, 0);
65 | t.true(marky.emit.calledWith('markychange'));
66 | marky.emit.restore();
67 | t.end();
68 | });
69 |
70 | test('Marky > redo', (t) => {
71 | sinon.spy(marky, 'emit');
72 | marky.store.index = 0;
73 | marky.redo(1);
74 |
75 | t.equal(marky.store.index, 1);
76 | t.true(marky.emit.calledWith('markychange'));
77 | marky.emit.restore();
78 | t.end();
79 | });
80 |
81 | test('Marky > setSelection', (t) => {
82 | editor.value = 'Some text';
83 | editor.setSelectionRange(0, 0);
84 | marky.setSelection([0, 9]);
85 |
86 | t.equal(editor.selectionStart, 0);
87 | t.equal(editor.selectionEnd, 9);
88 | t.end();
89 | });
90 |
91 | test('Marky > expandSelectionForward', (t) => {
92 | editor.value = 'Some text';
93 | editor.setSelectionRange(0, 0);
94 | marky.expandSelectionForward(1);
95 |
96 | t.equal(editor.selectionStart, 0);
97 | t.equal(editor.selectionEnd, 1);
98 | t.end();
99 | });
100 |
101 | test('Marky > expandSelectionBackward', (t) => {
102 | editor.value = 'Some text';
103 | editor.setSelectionRange(9, 9);
104 | marky.expandSelectionBackward(1);
105 |
106 | t.equal(editor.selectionStart, 8);
107 | t.equal(editor.selectionEnd, 9);
108 | t.end();
109 | });
110 |
111 | test('Marky > moveCursorForward', (t) => {
112 | editor.value = 'Some text';
113 | editor.setSelectionRange(0, 0);
114 | marky.moveCursorForward(1);
115 |
116 | t.equal(editor.selectionStart, 1);
117 | t.equal(editor.selectionEnd, 1);
118 | t.end();
119 | });
120 |
121 | test('Marky > moveCursorBackward', (t) => {
122 | editor.value = 'Some text';
123 | editor.setSelectionRange(9, 9);
124 | marky.moveCursorBackward(1);
125 |
126 | t.equal(editor.selectionStart, 8);
127 | t.equal(editor.selectionEnd, 8);
128 | t.end();
129 | });
130 |
131 | test('Marky > bold', (t) => {
132 | sinon.spy(marky, 'emit');
133 | editor.value = 'Some text';
134 | editor.setSelectionRange(0, 9);
135 | marky.bold();
136 |
137 | t.equal(marky.state.html, ' Some text
\n');
138 | t.true(marky.emit.calledWith('markychange'));
139 | marky.emit.restore();
140 | t.end();
141 | });
142 |
143 | test('Marky > italic', (t) => {
144 | sinon.spy(marky, 'emit');
145 | editor.value = 'Some text';
146 | editor.setSelectionRange(0, 9);
147 | marky.italic();
148 |
149 | t.equal(marky.state.html, 'Some text
\n');
150 | t.true(marky.emit.calledWith('markychange'));
151 | marky.emit.restore();
152 | t.end();
153 | });
154 |
155 | test('Marky > strikethrough', (t) => {
156 | sinon.spy(marky, 'emit');
157 | editor.value = 'Some text';
158 | editor.setSelectionRange(0, 9);
159 | marky.strikethrough();
160 |
161 | t.equal(marky.state.html, 'Some text
\n');
162 | t.true(marky.emit.calledWith('markychange'));
163 | marky.emit.restore();
164 | t.end();
165 | });
166 |
167 | test('Marky > code', (t) => {
168 | sinon.spy(marky, 'emit');
169 | editor.value = 'Some text';
170 | editor.setSelectionRange(0, 9);
171 | marky.code();
172 |
173 | t.equal(marky.state.html, 'Some text
\n');
174 | t.true(marky.emit.calledWith('markychange'));
175 | marky.emit.restore();
176 | t.end();
177 | });
178 |
179 | test('Marky > blockquote', (t) => {
180 | sinon.spy(marky, 'emit');
181 | editor.value = 'Some text';
182 | editor.setSelectionRange(0, 9);
183 | marky.blockquote();
184 |
185 | t.equal(marky.state.html, '\nSome text
\n \n');
186 | t.true(marky.emit.calledWith('markychange'));
187 | marky.emit.restore();
188 | t.end();
189 | });
190 |
191 | test('Marky > heading', (t) => {
192 | sinon.spy(marky, 'emit');
193 | editor.value = 'Some text';
194 | editor.setSelectionRange(0, 9);
195 | marky.heading(1);
196 |
197 | t.equal(marky.state.html, 'Some text \n');
198 | t.true(marky.emit.calledWith('markyupdate'));
199 | marky.emit.restore();
200 | t.end();
201 | });
202 |
203 | test('Marky > heading with a default of 0', (t) => {
204 | sinon.spy(marky, 'emit');
205 | editor.value = '# Some text';
206 | editor.setSelectionRange(2, 9);
207 | marky.heading();
208 |
209 | t.equal(marky.state.html, 'Some text
\n');
210 | t.true(marky.emit.calledWith('markyupdate'));
211 |
212 | marky.emit.restore();
213 | t.end();
214 | });
215 |
216 | test('Marky > link', (t) => {
217 | sinon.spy(marky, 'emit');
218 | editor.value = 'Some text';
219 | editor.setSelectionRange(0, 9);
220 | marky.link([0, 9], 'http://google.com', 'Some text');
221 |
222 | t.equal(editor.value, '[Some text](http://google.com)');
223 | t.equal(editor.selectionStart, 0);
224 | t.equal(editor.selectionEnd, editor.value.length);
225 | t.true(marky.emit.calledWith('markyupdate'));
226 |
227 | marky.emit.restore();
228 | t.end();
229 | });
230 |
231 | test('Marky > link default snippet', (t) => {
232 | sinon.spy(marky, 'emit');
233 | editor.value = 'Some text';
234 | editor.setSelectionRange(0, 9);
235 | marky.link();
236 |
237 | t.equal(editor.value, '[http://url.com](http://url.com)');
238 | t.equal(editor.selectionStart, 0);
239 | t.equal(editor.selectionEnd, editor.value.length);
240 | t.true(marky.emit.calledWith('markyupdate'));
241 | marky.emit.restore();
242 | t.end();
243 | });
244 |
245 | test('Marky > image', (t) => {
246 | sinon.spy(marky, 'emit');
247 | editor.value = 'Some text';
248 | editor.setSelectionRange(0, 9);
249 | marky.image([0, 9], 'http://i.imgur.com/VlVsP.gif', 'Chuck Chardonnay');
250 |
251 | t.equal(editor.value, '');
252 | t.equal(editor.selectionStart, 0);
253 | t.equal(editor.selectionEnd, editor.value.length);
254 | t.true(marky.emit.calledWith('markyupdate'));
255 | marky.emit.restore();
256 | t.end();
257 | });
258 |
259 | test('Marky > image default snippet', (t) => {
260 | sinon.spy(marky, 'emit');
261 | editor.value = 'Some text';
262 | editor.setSelectionRange(0, 9);
263 | marky.image();
264 |
265 | t.equal(editor.value, '');
266 | t.equal(editor.selectionStart, 0);
267 | t.equal(editor.selectionEnd, editor.value.length);
268 | t.true(marky.emit.calledWith('markyupdate'));
269 | marky.emit.restore();
270 | t.end();
271 | });
272 |
273 | test('Marky > unorderedList', (t) => {
274 | sinon.spy(marky, 'emit');
275 | editor.value = 'Some text\r\nSome other text';
276 | editor.setSelectionRange(0, 26);
277 | marky.unorderedList();
278 |
279 | t.equal(editor.value, '- Some text\n- Some other text');
280 | t.true(marky.emit.calledWith('markyupdate'));
281 | marky.emit.restore();
282 | t.end();
283 | });
284 |
285 | test('Marky > orderedList', (t) => {
286 | sinon.spy(marky, 'emit');
287 | editor.value = 'Some text\r\nSome other text';
288 | editor.setSelectionRange(0, 26);
289 | marky.orderedList();
290 |
291 | t.equal(editor.value, '1. Some text\n2. Some other text');
292 | t.true(marky.emit.calledWith('markyupdate'));
293 | marky.emit.restore();
294 | t.end();
295 | });
296 |
297 | test('Marky > indent', (t) => {
298 | editor.value = '- Some text\r\n- Some other text';
299 | editor.setSelectionRange(0, 30);
300 | marky.indent();
301 |
302 | t.equal(editor.value, ' - Some text\n - Some other text');
303 | t.end();
304 | });
305 |
306 | test('Marky > outdent', (t) => {
307 | editor.value = ' - Some text\r\n - Some other text';
308 | editor.setSelectionRange(0, 38);
309 | marky.outdent();
310 |
311 | t.equal(editor.value, '- Some text\n- Some other text');
312 | t.end();
313 | });
314 |
315 | test('Marky > undoes state', (t) => {
316 | const timeline = [
317 | {
318 | markdown: '',
319 | html: '',
320 | selection: [0, 0],
321 | },
322 | {
323 | markdown: 'Some text',
324 | html: 'Some text
',
325 | selection: [0, 0],
326 | },
327 | ];
328 | const index = 1;
329 | marky.store.timeline = timeline;
330 | marky.store.index = index;
331 |
332 | t.equal(marky.undo(1), 0);
333 | t.end();
334 | });
335 |
336 | test('Marky > does not undo state if state is at 0 index', (t) => {
337 | const timeline = [
338 | {
339 | markdown: '',
340 | html: '',
341 | selection: [0, 0],
342 | },
343 | {
344 | markdown: 'Some text',
345 | html: 'Some text
',
346 | selection: [0, 0],
347 | },
348 | ];
349 | const index = 0;
350 | marky.store.timeline = timeline;
351 | marky.store.index = index;
352 |
353 | t.equal(marky.undo(1), 0);
354 | t.end();
355 | });
356 |
357 | test('Marky > redoes state', (t) => {
358 | const timeline = [
359 | {
360 | markdown: '',
361 | html: '',
362 | selection: [0, 0],
363 | },
364 | {
365 | markdown: 'Some text',
366 | html: 'Some text
',
367 | selection: [0, 0],
368 | },
369 | ];
370 | const index = 0;
371 | marky.store.timeline = timeline;
372 | marky.store.index = index;
373 |
374 | t.equal(marky.redo(1), 1);
375 | t.end();
376 | });
377 |
378 | test('Marky > does not redo state if state is at last index', (t) => {
379 | const timeline = [
380 | {
381 | markdown: '',
382 | html: '',
383 | selection: [0, 0],
384 | },
385 | {
386 | markdown: 'Some text',
387 | html: 'Some text
',
388 | selection: [0, 0],
389 | },
390 | ];
391 | const index = 1;
392 | marky.store.timeline = timeline;
393 | marky.store.index = index;
394 |
395 | t.equal(marky.redo(1), 1);
396 | t.end();
397 | });
398 |
--------------------------------------------------------------------------------
/test/Store.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import Store from '../src/Store';
3 |
4 | test('Store > update > handles updating state', (t) => {
5 | const store = new Store([{ markdown: '', html: '', selection: [0, 0] }]);
6 | store.update('Some text', [9, 9]);
7 |
8 | t.equal(store.timeline.length, 2);
9 | t.equal(store.timeline[1].markdown, 'Some text');
10 | t.true(store.timeline[1].html.includes('Some text
'));
11 | t.deepEqual(store.timeline[1].selection, [9, 9]);
12 | t.equal(store.timeline[0].markdown, '');
13 | t.deepEqual(store.timeline[0].selection, [0, 0]);
14 | t.equal(store.index, 1);
15 | t.end();
16 | });
17 |
18 | test('Store > update > adds to existing state', (t) => {
19 | const store = new Store([
20 | { markdown: '', html: '', selection: [0, 0] },
21 | { markdown: 'Some text', html: 'Some text
', selection: [9, 9] },
22 | ]);
23 | store.index = 1;
24 | store.update('', [0, 0]);
25 |
26 | t.equal(store.timeline.length, 3);
27 | t.equal(store.timeline[2].markdown, '');
28 | t.equal(store.timeline[2].html, '');
29 | t.deepEqual(store.timeline[2].selection, [0, 0]);
30 | t.equal(store.timeline[1].markdown, 'Some text');
31 | t.deepEqual(store.timeline[1].selection, [9, 9]);
32 | t.equal(store.index, 2);
33 | t.end();
34 | });
35 |
36 | test('Store > update > removes old states when there are 1000 of them', (t) => {
37 | const store = new Store([
38 | { markdown: '', html: '', selection: [0, 0] },
39 | { markdown: 'Some text', html: 'Some text
', selection: [9, 9] },
40 | ]);
41 | store.index = 999;
42 | store.update('', [0, 0]);
43 |
44 | t.equal(store.timeline.length, 2);
45 | t.equal(store.timeline[1].markdown, '');
46 | t.equal(store.timeline[1].html, '');
47 | t.equal(store.timeline[0].markdown, 'Some text');
48 | t.equal(store.index, 999);
49 | t.end();
50 | });
51 |
52 | let timelineMock = [
53 | {
54 | markdown: '',
55 | html: '',
56 | selection: [0, 0],
57 | },
58 | {
59 | markdown: 'Some text',
60 | html: 'Some text
',
61 | selection: [0, 0],
62 | },
63 | {
64 | markdown: 'Some funny text',
65 | html: 'Some funny text
',
66 | selection: [0, 0],
67 | },
68 | {
69 | markdown: 'Some really funny text',
70 | html: 'Some really funny text
',
71 | selection: [0, 0],
72 | },
73 | {
74 | markdown: 'Some really funny awesome text',
75 | html: 'Some really funny awesome text
',
76 | selection: [0, 0],
77 | },
78 | {
79 | markdown: 'Some really funny awesome crazy text',
80 | html: 'Some really funny awesome crazy text
',
81 | selection: [0, 0],
82 | },
83 | {
84 | markdown: 'Some really super funny awesome crazy text',
85 | html: 'Some really super funny awesome crazy text
',
86 | selection: [0, 0],
87 | },
88 | ];
89 |
90 | test('Store > redo > returns a future state', (t) => {
91 | const store = new Store(timelineMock);
92 | store.redo(5);
93 |
94 | t.equal(store.state.markdown, 'Some really funny awesome crazy text');
95 | t.equal(store.state.html, 'Some really funny awesome crazy text
');
96 | t.deepEqual(store.state.selection, [0, 0]);
97 | t.end();
98 | });
99 |
100 | test('Store > redo > returns a future state from the middle of the stack', (t) => {
101 | const store = new Store([...timelineMock]);
102 | store.index = 1;
103 | store.redo(5);
104 |
105 | t.deepEqual(store.state, {
106 | markdown: 'Some really super funny awesome crazy text',
107 | html: 'Some really super funny awesome crazy text
',
108 | selection: [0, 0],
109 | });
110 | t.end();
111 | });
112 |
113 | test('Store > redo > returns the newest if it is less than the num specified from the end in the stack', (t) => {
114 | const store = new Store([...timelineMock]);
115 | store.index = 3;
116 | store.redo(5);
117 |
118 | t.deepEqual(store.state, {
119 | markdown: 'Some really super funny awesome crazy text',
120 | html: 'Some really super funny awesome crazy text
',
121 | selection: [0, 0],
122 | });
123 | t.end();
124 | });
125 |
126 | timelineMock = [
127 | {
128 | markdown: '',
129 | html: '',
130 | selection: [0, 0],
131 | },
132 | {
133 | markdown: 'Some text',
134 | html: 'Some text
',
135 | selection: [0, 0],
136 | },
137 | {
138 | markdown: 'Some funny text',
139 | html: 'Some funny text
',
140 | selection: [0, 0],
141 | },
142 | {
143 | markdown: 'Some really funny text',
144 | html: 'Some really funny text
',
145 | selection: [0, 0],
146 | },
147 | {
148 | markdown: 'Some really funny awesome text',
149 | html: 'Some really funny awesome text
',
150 | selection: [0, 0],
151 | },
152 | {
153 | markdown: 'Some really funny awesome crazy text',
154 | html: 'Some really funny awesome crazy text
',
155 | selection: [0, 0],
156 | },
157 | {
158 | markdown: 'Some really super funny awesome crazy text',
159 | html: 'Some really super funny awesome crazy text
',
160 | selection: [0, 0],
161 | },
162 | ];
163 |
164 | test('Store > undo > returns a previous state', (t) => {
165 | const store = new Store([...timelineMock]);
166 | store.index = 5;
167 | store.undo(1);
168 |
169 | t.deepEqual(store.state, {
170 | markdown: 'Some really funny awesome text',
171 | html: 'Some really funny awesome text
',
172 | selection: [0, 0],
173 | });
174 | t.end();
175 | });
176 |
177 | test('Store > undo > returns a previous state from the middle of the stack', (t) => {
178 | const store = new Store([...timelineMock]);
179 | store.index = 5;
180 | store.undo(5);
181 |
182 | t.deepEqual(store.state, {
183 | markdown: '',
184 | html: '',
185 | selection: [0, 0],
186 | });
187 | t.end();
188 | });
189 |
190 | test('Store > undo > returns oldest state if state index is less than the num specified', (t) => {
191 | const store = new Store([...timelineMock]);
192 | store.index = 3;
193 | store.undo(5);
194 |
195 | t.deepEqual(store.state, {
196 | markdown: '',
197 | html: '',
198 | selection: [0, 0],
199 | });
200 | t.end();
201 | });
202 |
--------------------------------------------------------------------------------
/test/buttons.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import initializer from '../src/initializer';
3 |
4 | const container = document.createElement('marky-mark');
5 | document.body.appendChild(container);
6 | const marky = initializer(container);
7 | const { editor } = marky;
8 |
9 | const timelineMock = [
10 | {
11 | markdown: '',
12 | html: '',
13 | selection: [0, 0],
14 | },
15 | {
16 | markdown: 'Some text',
17 | html: 'Some text
',
18 | selection: [0, 0],
19 | },
20 | {
21 | markdown: 'Some funny text',
22 | html: 'Some funny text
',
23 | selection: [0, 0],
24 | },
25 | {
26 | markdown: 'Some really funny text',
27 | html: 'Some really funny text
',
28 | selection: [0, 0],
29 | },
30 | {
31 | markdown: 'Some really funny awesome text',
32 | html: 'Some really funny awesome text
',
33 | selection: [0, 0],
34 | },
35 | {
36 | markdown: 'Some really funny awesome crazy text',
37 | html: 'Some really funny awesome crazy text
',
38 | selection: [0, 0],
39 | },
40 | {
41 | markdown: 'Some really super funny awesome crazy text',
42 | html: 'Some really super funny awesome crazy text
',
43 | selection: [0, 0],
44 | },
45 | ];
46 |
47 | test('buttons > controls the heading dialog', (t) => {
48 | editor.value = 'Some text';
49 | editor.setSelectionRange(0, 9);
50 | container.querySelector('.image').click();
51 | container.querySelector('.link').click();
52 | container.querySelector('.heading').click();
53 | t.true(container.querySelector('.heading-dialog').classList.contains('toggled'));
54 | t.false(container.querySelector('.link-dialog').classList.contains('toggled'));
55 | t.false(container.querySelector('.image-dialog').classList.contains('toggled'));
56 | t.end();
57 | });
58 |
59 | test('buttons > calls the bold method', (t) => {
60 | editor.value = 'Some text';
61 | editor.setSelectionRange(0, 9);
62 | container.querySelector('.bold').click();
63 | t.equal(editor.value, '**Some text**');
64 | t.end();
65 | });
66 |
67 | test('buttons > calls the italic method', (t) => {
68 | editor.value = 'Some text';
69 | editor.setSelectionRange(0, 9);
70 | container.querySelector('.italic').click();
71 | t.equal(editor.value, '_Some text_');
72 | t.end();
73 | });
74 |
75 | test('buttons > calls the strikethrough method', (t) => {
76 | editor.value = 'Some text';
77 | editor.setSelectionRange(0, 9);
78 | container.querySelector('.strikethrough').click();
79 | t.equal(editor.value, '~~Some text~~');
80 | t.end();
81 | });
82 |
83 | test('buttons > calls the code method', (t) => {
84 | editor.value = 'Some text';
85 | editor.setSelectionRange(0, 9);
86 | container.querySelector('.code').click();
87 | t.equal(editor.value, '`Some text`');
88 | t.end();
89 | });
90 |
91 | test('buttons > calls the blockquote method', (t) => {
92 | editor.value = 'Some text';
93 | editor.setSelectionRange(0, 9);
94 | container.querySelector('.blockquote').click();
95 | t.equal(editor.value, '> Some text');
96 | t.end();
97 | });
98 |
99 | test('buttons > controls the link dialog', (t) => {
100 | editor.value = 'Some text';
101 | editor.setSelectionRange(0, 9);
102 | container.querySelector('.image').click();
103 | container.querySelector('.link').click();
104 | t.true(container.querySelector('.link-dialog').classList.contains('toggled'));
105 | t.false(container.querySelector('.image-dialog').classList.contains('toggled'));
106 | t.end();
107 | });
108 |
109 | test('buttons > automatically assigns the value of the link display text', (t) => {
110 | editor.value = 'Some text';
111 | editor.setSelectionRange(0, 9);
112 | container.querySelector('.link').click();
113 |
114 | t.equal(container.querySelector('.link-display-input').value, 'Some text');
115 | t.end();
116 | });
117 |
118 | test('buttons > controls the image dialog', (t) => {
119 | editor.value = 'Some text';
120 | editor.setSelectionRange(0, 9);
121 | container.querySelector('.link').click();
122 | container.querySelector('.image').click();
123 | t.true(container.querySelector('.image-dialog').classList.contains('toggled'));
124 | t.false(container.querySelector('.link-dialog').classList.contains('toggled'));
125 | t.end();
126 | });
127 |
128 | test('buttons > automatically assigns the value of the image alt text', (t) => {
129 | editor.value = 'Some text';
130 | editor.setSelectionRange(0, 9);
131 | container.querySelector('.image').click();
132 |
133 | t.equal(container.querySelector('.image-alt-input').value, 'Some text');
134 | t.end();
135 | });
136 |
137 | test('buttons > calls the unorderedList method', (t) => {
138 | editor.value = 'Some text\r\nSome other text';
139 | editor.setSelectionRange(0, 26);
140 | container.querySelector('.unordered-list').click();
141 | t.equal(editor.value, '- Some text\n- Some other text');
142 | t.end();
143 | });
144 |
145 | test('buttons > calls the ordered list method', (t) => {
146 | editor.value = 'Some text\r\nSome other text';
147 | editor.setSelectionRange(0, 26);
148 | container.querySelector('.ordered-list').click();
149 | t.equal(editor.value, '1. Some text\n2. Some other text');
150 | t.end();
151 | });
152 |
153 | test('buttons > calls the indent method', (t) => {
154 | editor.value = '- Some text\r\n- Some other text';
155 | editor.setSelectionRange(0, 30);
156 | container.querySelector('.indent').click();
157 | t.equal(editor.value, ' - Some text\n - Some other text');
158 | t.end();
159 | });
160 |
161 | test('buttons > calls the outdent method', (t) => {
162 | editor.value = ' - Some text\r\n - Some other text';
163 | editor.setSelectionRange(0, 38);
164 | container.querySelector('.outdent').click();
165 | t.equal(editor.value, '- Some text\n- Some other text');
166 | t.end();
167 | });
168 |
169 | test('buttons > calls the undo method', (t) => {
170 | marky.store.timeline = timelineMock;
171 | marky.store.index = 5;
172 | editor.value = timelineMock[5].markdown;
173 |
174 | container.querySelector('.undo').click();
175 |
176 | t.equal(editor.value, timelineMock[4].markdown);
177 | t.end();
178 | });
179 |
180 | test('buttons > does not call the undo method if disabled', (t) => {
181 | marky.store.timeline = timelineMock;
182 | marky.store.index = 6;
183 | editor.value = timelineMock[6].markdown;
184 |
185 | container.querySelector('.undo').classList.add('disabled');
186 | container.querySelector('.undo').click();
187 |
188 | t.equal(editor.value, timelineMock[6].markdown);
189 | t.end();
190 | });
191 |
192 | test('buttons > calls the redo method', (t) => {
193 | marky.store.timeline = timelineMock;
194 | marky.store.index = 0;
195 | editor.value = timelineMock[0].markdown;
196 |
197 | container.querySelector('.redo').click();
198 |
199 | t.equal(editor.value, timelineMock[1].markdown);
200 | t.end();
201 | });
202 |
203 | test('buttons > does not call the redo method if disabled', (t) => {
204 | marky.store.timeline = timelineMock;
205 | marky.store.index = 0;
206 | editor.value = timelineMock[0].markdown;
207 |
208 | container.querySelector('.redo').classList.add('disabled');
209 | container.querySelector('.redo').click();
210 |
211 | t.equal(editor.value, timelineMock[0].markdown);
212 | t.end();
213 | });
214 |
215 | test('buttons > turns on expanded view', (t) => {
216 | container.querySelector('.expand').click();
217 |
218 | t.true(container.classList.contains('marky-expanded'));
219 | t.true(editor.classList.contains('marky-expanded'));
220 | t.end();
221 | });
222 |
223 | test('buttons > turns off expanded', (t) => {
224 | container.querySelector('.expand').click();
225 |
226 | t.false(container.classList.contains('marky-expanded'));
227 | t.false(editor.classList.contains('marky-expanded'));
228 | t.end();
229 | });
230 |
--------------------------------------------------------------------------------
/test/dialogs.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import initializer from '../src/initializer';
3 |
4 | const container = document.createElement('marky-mark');
5 | document.body.appendChild(container);
6 | const marky = initializer(container);
7 | const { editor } = marky;
8 |
9 | test('dialogs > calls the image method', (t) => {
10 | editor.value = 'Some text';
11 | editor.setSelectionRange(0, 9);
12 | const source = container.querySelector('.image-source-input');
13 | const alt = container.querySelector('.image-alt-input');
14 | source.value = 'http://i.imgur.com/VlVsP.gif';
15 | alt.value = 'Chuck Chardonnay';
16 | container.querySelector('.insert-image').click();
17 |
18 | t.equal(editor.value, '');
19 | t.end();
20 | });
21 |
22 | test('dialogs > calls the link method', (t) => {
23 | editor.value = 'Some text';
24 | editor.setSelectionRange(0, 9);
25 | const source = container.querySelector('.link-url-input');
26 | const alt = container.querySelector('.link-display-input');
27 | source.value = 'http://google.com';
28 | alt.value = 'Google';
29 | container.querySelector('.insert-link').click();
30 |
31 | t.equal(editor.value, '[Google](http://google.com)');
32 | t.end();
33 | });
34 |
--------------------------------------------------------------------------------
/test/elements/Button.spec.js:
--------------------------------------------------------------------------------
1 | /* global HTMLElement */
2 |
3 | import test from 'tape';
4 | import Button from '../../src/elements/Button';
5 |
6 | test('Button > creates a button', (t) => {
7 | const button = new Button('bold', 'Bold', 'fa', 'fa-bold');
8 |
9 | t.true(button.element instanceof HTMLElement);
10 | t.equal(button.element.title, 'Bold');
11 | t.equal(button.element.tagName.toLowerCase(), 'button');
12 | t.ok(button.element.querySelector('i'));
13 | t.true(button.element.querySelector('i').classList.contains('fa-bold'));
14 | t.end();
15 | });
16 |
--------------------------------------------------------------------------------
/test/elements/Dialog.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import Dialog from '../../src/elements/Dialog';
3 |
4 | test('Dialog > creates a dialog', (t) => {
5 | const dialog = new Dialog('dialog', 'Dialog');
6 |
7 | t.true(dialog.element instanceof HTMLElement);
8 | t.equal(dialog.element.title, 'Dialog');
9 | t.end();
10 | });
11 |
--------------------------------------------------------------------------------
/test/elements/Element.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import Element from '../../src/elements/Element';
3 |
4 | test('Element > creates an element', (t) => {
5 | const element = new Element('div', { title: 'element' });
6 |
7 | t.true(element.element instanceof HTMLElement);
8 | t.equal(element.element.title, 'element');
9 | t.end();
10 | });
11 |
--------------------------------------------------------------------------------
/test/elements/HeadingDialog.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import HeadingDialog from '../../src/elements/HeadingDialog';
3 |
4 | test('HeadingDialog > creates a heading dialog', (t) => {
5 | const headingDialog = new HeadingDialog('heading-dialog', 'Heading Dialog');
6 |
7 | t.true(headingDialog.element instanceof HTMLElement);
8 | t.equal(headingDialog.element.title, 'Heading Dialog');
9 | t.ok(headingDialog.element.querySelector('.heading-dialog-heading-list'));
10 | t.equal(headingDialog.element.querySelectorAll('button').length, 7);
11 | t.end();
12 | });
13 |
--------------------------------------------------------------------------------
/test/elements/HeadingItem.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import HeadingItem from '../../src/elements/HeadingItem';
3 |
4 | test('HeadingItem > creates a heading item', (t) => {
5 | const headingItem = new HeadingItem('Heading 1', 1);
6 |
7 | t.true(headingItem.element instanceof HTMLElement);
8 | t.equal(headingItem.element.title, 'Heading 1');
9 | t.equal(headingItem.element.textContent, '1');
10 | t.equal(headingItem.element.value, 1);
11 | t.equal(headingItem.element.tagName.toLowerCase(), 'li');
12 | t.end();
13 | });
14 |
15 | test('HeadingItem > creates a heading item with icon', (t) => {
16 | const headingItem = new HeadingItem('Heading 1', 0, 'fa', 'fa-times');
17 |
18 | t.true(headingItem.element instanceof HTMLElement);
19 | t.ok(headingItem.element.querySelector('i'));
20 | t.true(headingItem.element.querySelector('i').classList.contains('fa-times'));
21 | t.end();
22 | });
23 |
--------------------------------------------------------------------------------
/test/elements/Icon.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import Icon from '../../src/elements/Icon';
3 |
4 | test('Icon > creates an icon', (t) => {
5 | const icon = new Icon('fa', 'fa-cog');
6 |
7 | t.true(icon.element instanceof HTMLElement);
8 | t.true(icon.element.classList.contains('fa'));
9 | t.true(icon.element.classList.contains('fa-cog'));
10 | t.equal(icon.element.tagName.toLowerCase(), 'i');
11 | t.end();
12 | });
13 |
--------------------------------------------------------------------------------
/test/elements/ImageDialog.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import ImageDialog from '../../src/elements/ImageDialog';
3 |
4 | test('ImageDialog > creates an image dialog', (t) => {
5 | const imageDialog = new ImageDialog('image-dialog', 'Image Dialog');
6 |
7 | t.true(imageDialog.element instanceof HTMLElement);
8 | t.equal(imageDialog.element.title, 'Image Dialog');
9 | t.ok(imageDialog.element.querySelector('#image-dialog-image-form'));
10 | t.ok(imageDialog.element.querySelector('.image-source-input'));
11 | t.ok(imageDialog.element.querySelector('.image-alt-input'));
12 | t.ok(imageDialog.element.querySelector('.insert-image'));
13 | t.end();
14 | });
15 |
--------------------------------------------------------------------------------
/test/elements/LinkDialog.spec.js:
--------------------------------------------------------------------------------
1 | // import test from 'tape'
2 | // import LinkDialog from '../../src/elements/LinkDialog'
3 |
4 | // test('LinkDialog > creates a link dialog', (t) => {
5 | // const linkDialog = Object.create(LinkDialog)
6 | // .init('Link Dialog', 'link-dialog')
7 |
8 | // t.true(linkDialog.element instanceof HTMLElement)
9 | // t.equal(linkDialog.element.title, 'Link Dialog')
10 | // t.ok(linkDialog.element.querySelector('#link-dialog-link-form'))
11 | // t.ok(linkDialog.element.querySelector('.link-url-input'))
12 | // t.ok(linkDialog.element.querySelector('.link-display-input'))
13 | // t.ok(linkDialog.element.querySelector('.insert-link'))
14 | // t.end()
15 | // })
16 |
--------------------------------------------------------------------------------
/test/elements/Separator.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import Separator from '../../src/elements/Separator';
3 |
4 | test('Separator > creates a separator', (t) => {
5 | const separator = new Separator();
6 |
7 | t.true(separator.element instanceof HTMLElement);
8 | t.true(separator.element.classList.contains('separator'));
9 | t.equal(separator.element.tagName.toLowerCase(), 'span');
10 | t.end();
11 | });
12 |
--------------------------------------------------------------------------------
/test/headings.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import initializer from '../src/initializer';
3 |
4 | const container = document.createElement('marky-mark');
5 | document.body.appendChild(container);
6 | const marky = initializer(container);
7 | const { editor } = marky;
8 |
9 | test('headings > calls the heading-1 method', (t) => {
10 | editor.value = 'Some text';
11 | editor.setSelectionRange(0, 9);
12 | const heading1 = container.querySelector('.heading-1').children[0];
13 | heading1.click();
14 |
15 | t.equal(editor.value, '# Some text');
16 | t.end();
17 | });
18 |
19 | test('headings > calls the heading-2 method', (t) => {
20 | editor.value = 'Some text';
21 | editor.setSelectionRange(0, 9);
22 | const heading2 = container.querySelector('.heading-2').children[0];
23 | heading2.click();
24 |
25 | t.equal(editor.value, '## Some text');
26 | t.end();
27 | });
28 |
29 | test('headings > calls the heading-6 method', (t) => {
30 | editor.value = 'Some text';
31 | editor.setSelectionRange(0, 9);
32 | const heading6 = container.querySelector('.heading-6').children[0];
33 | heading6.click();
34 |
35 | t.equal(editor.value, '###### Some text');
36 | t.end();
37 | });
38 |
39 | test('headings > removes any existing heading', (t) => {
40 | editor.value = 'Some text';
41 | editor.setSelectionRange(0, 9);
42 | const removeHeading = container.querySelector('.remove-heading').children[0];
43 | removeHeading.click();
44 |
45 | t.equal(editor.value, 'Some text');
46 | t.end();
47 | });
48 |
--------------------------------------------------------------------------------
/test/initializer.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import initializer from '../src/initializer';
3 |
4 | test('initializer > assigns a Marky instance to the container', (t) => {
5 | const container = document.createElement('marky-mark');
6 | document.body.appendChild(container);
7 | initializer(container);
8 |
9 | t.ok(container.marky);
10 | document.body.removeChild(container);
11 | t.end();
12 | });
13 |
14 | test('initializer > creates a toolbar, a textarea, and a hidden input', (t) => {
15 | const container = document.createElement('marky-mark');
16 | document.body.appendChild(container);
17 | initializer(container);
18 |
19 | t.ok(container.querySelector('.marky-toolbar'));
20 | t.ok(container.querySelector('.marky-editor'));
21 | t.ok(container.querySelector('.heading'));
22 | t.ok(container.querySelector('.separator'));
23 | t.ok(container.querySelector('.bold'));
24 | t.ok(container.querySelector('.italic'));
25 | t.ok(container.querySelector('.strikethrough'));
26 | t.ok(container.querySelector('.code'));
27 | t.ok(container.querySelector('.blockquote'));
28 | t.ok(container.querySelector('.link'));
29 | t.ok(container.querySelector('.image'));
30 | t.ok(container.querySelector('.unordered-list'));
31 | t.ok(container.querySelector('.ordered-list'));
32 | t.ok(container.querySelector('.outdent'));
33 | t.ok(container.querySelector('.indent'));
34 | t.ok(container.querySelector('.undo'));
35 | t.ok(container.querySelector('.redo'));
36 | t.ok(container.querySelector('.expand'));
37 | document.body.removeChild(container);
38 | t.end();
39 | });
40 |
41 | test('initializer > initializes on an empty element passed in', (t) => {
42 | const container = document.createElement('mark-wahlberg');
43 | document.body.appendChild(container);
44 |
45 | t.doesNotThrow(() => initializer(container), TypeError);
46 |
47 | document.body.removeChild(container);
48 | t.end();
49 | });
50 |
51 | test('initializer > initializes on a NodeList item passed in', (t) => {
52 | const container = document.createElement('mark-wahlberg');
53 | document.body.appendChild(container);
54 |
55 | t.doesNotThrow(() => initializer(document.querySelectorAll('mark-wahlberg')[0]), TypeError);
56 |
57 | document.body.removeChild(container);
58 | t.end();
59 | });
60 |
61 | test('initializer > initializes on an HTMLCollection item passed in', (t) => {
62 | const container = document.createElement('mark-wahlberg');
63 | document.body.appendChild(container);
64 |
65 | t.doesNotThrow(() => initializer(document.getElementsByTagName('mark-wahlberg')[0]), TypeError);
66 |
67 | document.body.removeChild(container);
68 | t.end();
69 | });
70 |
71 | test('initializer > returns a marky instance', (t) => {
72 | const container = document.createElement('mark-wahlberg');
73 | document.body.appendChild(container);
74 |
75 | t.ok(initializer(container).state);
76 |
77 | document.body.removeChild(container);
78 | t.end();
79 | });
80 |
81 | test('initializer > assigns the marky instance to the container', (t) => {
82 | const container = document.createElement('mark-wahlberg');
83 | document.body.appendChild(container);
84 |
85 | initializer(container);
86 | t.ok(container.marky);
87 |
88 | document.body.removeChild(container);
89 | t.end();
90 | });
91 |
92 | test('initializer > adds elements to the Marky instance', (t) => {
93 | const container = document.createElement('marky-mark');
94 | document.body.appendChild(container);
95 |
96 | const marky = initializer(container);
97 |
98 | t.true(Object.keys(marky.elements.dialogs).length > 0);
99 | t.true(Object.keys(marky.elements.buttons).length > 0);
100 | t.ok(marky.elements.editor);
101 |
102 | document.body.removeChild(container);
103 | t.end();
104 | });
105 |
106 | test('initializer > adds listeners to the Marky instance', (t) => {
107 | const container = document.createElement('marky-mark');
108 | document.body.appendChild(container);
109 |
110 | const marky = initializer(container);
111 |
112 | t.true(Object.keys(marky.elements.dialogs.heading.options[0].listeners).length > 0);
113 | t.true(Object.keys(marky.elements.dialogs.image.form.listeners).length > 0);
114 | t.true(Object.keys(marky.elements.dialogs.link.form.listeners).length > 0);
115 | t.true(
116 | Object.keys(marky.elements.buttons).every(buttonName => (
117 | Object.keys(marky.elements.buttons[buttonName].listeners).length > 0
118 | )),
119 | );
120 | t.true(Object.keys(marky.elements.editor.listeners).length > 0);
121 |
122 | document.body.removeChild(container);
123 | t.end();
124 | });
125 |
126 | test('initializer > throws a TypeError if an array of non-HTMLElements is passed in', (t) => {
127 | t.throws(() => initializer('marky-mark'), TypeError);
128 | t.end();
129 | });
130 |
131 | test('initializer > does not initialize on non-empty elements', (t) => {
132 | const container = document.createElement('marky-mark');
133 | const child = document.createElement('div');
134 | container.appendChild(child);
135 | document.body.appendChild(container);
136 |
137 | initializer(container);
138 |
139 | t.equal(container.children.length, 1);
140 | t.notOk(container.querySelector('.marky-editor'));
141 | t.end();
142 | });
143 |
--------------------------------------------------------------------------------
/test/markymark.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import markymark from '../src';
3 |
4 | test('markymark > throws a TypeError if an invalid argument is passed in', (t) => {
5 | const containers = {};
6 |
7 | t.throws(() => markymark(containers), TypeError);
8 | t.end();
9 | });
10 |
11 | test('markymark > initializes on multiple elements', (t) => {
12 | const container = document.createElement('marky-mark');
13 | document.body.appendChild(container);
14 | const anotherContainer = document.createElement('funky-bunch');
15 | document.body.appendChild(anotherContainer);
16 | const yetAnotherContainer = document.createElement('marky-mark');
17 | document.body.appendChild(yetAnotherContainer);
18 |
19 | markymark();
20 |
21 | t.ok(container.querySelector('.marky-editor'));
22 | t.notOk(anotherContainer.querySelector('.marky-editor'));
23 | t.ok(yetAnotherContainer.querySelector('.marky-editor'));
24 | t.end();
25 | });
26 |
27 | test('markymark > initializes on marky-mark elements by default', (t) => {
28 | const container = document.createElement('funky-bunch');
29 | document.body.appendChild(container);
30 |
31 | markymark();
32 |
33 | t.notOk(container.querySelector('.marky-editor'));
34 | t.notOk(container.querySelector('.marky-toolbar'));
35 | t.end();
36 | });
37 |
38 | test('markymark > initializes on an array of empty elements passed in', (t) => {
39 | const container = document.createElement('mark-wahlberg');
40 | document.body.appendChild(container);
41 |
42 | t.doesNotThrow(() => markymark([container]), TypeError);
43 |
44 | document.body.removeChild(container);
45 | t.end();
46 | });
47 |
48 | test('markymark > initializes on a NodeList passed in', (t) => {
49 | const container = document.createElement('mark-wahlberg');
50 | document.body.appendChild(container);
51 |
52 | t.doesNotThrow(() => markymark(document.querySelectorAll('mark-wahlberg')), TypeError);
53 |
54 | document.body.removeChild(container);
55 | t.end();
56 | });
57 |
58 | test('markymark > initializes on an HTMLCollection passed in', (t) => {
59 | const container = document.createElement('mark-wahlberg');
60 | document.body.appendChild(container);
61 |
62 | t.doesNotThrow(() => markymark(document.getElementsByTagName('mark-wahlberg')), TypeError);
63 |
64 | document.body.removeChild(container);
65 | t.end();
66 | });
67 |
68 | test('markymark > returns an array of marky instances', (t) => {
69 | const container = document.createElement('mark-wahlberg');
70 | document.body.appendChild(container);
71 |
72 | t.ok(markymark([container])[0].state);
73 |
74 | document.body.removeChild(container);
75 | t.end();
76 | });
77 |
--------------------------------------------------------------------------------
/test/utils/block-handler.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { blockHandler } from '../../src/utils/markdownHandlers';
3 |
4 | test('block-handler > adds a formatting string to the beginning of a line', (t) => {
5 | const string = 'Some text';
6 | const indices = [0, 9];
7 |
8 | const headingify = blockHandler(string, indices, '# ');
9 |
10 | t.equal(headingify.value, '# Some text');
11 | t.deepEqual(headingify.range, [2, 11]);
12 | t.end();
13 | });
14 |
15 | test('block-handler > adds a formatting string to the beginning of a line', (t) => {
16 | const string = 'Some text';
17 | const indices = [0, 9];
18 |
19 | const headingify = blockHandler(string, indices, '# ');
20 |
21 | t.equal(headingify.value, '# Some text');
22 | t.deepEqual(headingify.range, [2, 11]);
23 | t.end();
24 | });
25 |
26 | test('block-handler > does not matter where the selection is on that line', (t) => {
27 | const string = 'Some text';
28 | const indices = [9, 9];
29 |
30 | const quotify = blockHandler(string, indices, '> ');
31 |
32 | t.equal(quotify.value, '> Some text');
33 | t.deepEqual(quotify.range, [11, 11]);
34 | t.end();
35 | });
36 |
37 | test('block-handler > works with multi-line selections', (t) => {
38 | const string = 'Some text\r\nSome other text';
39 | const indices = [0, 26];
40 |
41 | const headingify = blockHandler(string, indices, '## ');
42 |
43 | t.equal(headingify.value, '## Some text\r\nSome other text');
44 | t.deepEqual(headingify.range, [3, 29]);
45 | t.end();
46 | });
47 |
48 | test('block-handler > ignores other lines around the selection', (t) => {
49 | const string = 'Some text\r\nSome other text';
50 | const indices = [11, 26];
51 |
52 | const headingify = blockHandler(string, indices, '# ');
53 |
54 | t.equal(headingify.value, 'Some text\r\n# Some other text');
55 | t.deepEqual(headingify.range, [13, 28]);
56 | t.end();
57 | });
58 |
59 | test('block-handler > removes all other block formatting', (t) => {
60 | const string = '# Some text';
61 | const indices = [0, 11];
62 |
63 | const headingify = blockHandler(string, indices, '### ');
64 |
65 | t.equal(headingify.value, '### Some text');
66 | t.deepEqual(headingify.range, [4, 13]);
67 | t.end();
68 | });
69 |
70 | test('block-handler > removes all other block formatting even if format string is directly touching text', (t) => {
71 | const string = '> Some text';
72 | const indices = [0, 10];
73 |
74 | const headingify = blockHandler(string, indices, '## ');
75 |
76 | t.equal(headingify.value, '## Some text');
77 | t.deepEqual(headingify.range, [3, 11]);
78 | t.end();
79 | });
80 |
81 | test('block-handler > considers inline formats to be text', (t) => {
82 | const string = '**Some text**';
83 | const indices = [0, 13];
84 |
85 | const headingify = blockHandler(string, indices, '## ');
86 |
87 | t.equal(headingify.value, '## **Some text**');
88 | t.deepEqual(headingify.range, [3, 16]);
89 | t.end();
90 | });
91 |
92 | test('block-handler > also considers inline formats to be text when removing other block formats', (t) => {
93 | const string = '> **Some text**';
94 | const indices = [0, 15];
95 |
96 | const headingify = blockHandler(string, indices, '## ');
97 |
98 | t.equal(headingify.value, '## **Some text**');
99 | t.deepEqual(headingify.range, [3, 16]);
100 | t.end();
101 | });
102 |
103 | test('block-handler > also considers list formats to be text when removing other block formats', (t) => {
104 | const string = '> 1. Some text';
105 | const indices = [0, 14];
106 |
107 | const headingify = blockHandler(string, indices, '> ');
108 |
109 | t.equal(headingify.value, '1. Some text');
110 | t.deepEqual(headingify.range, [0, 12]);
111 | t.end();
112 | });
113 |
114 | test('block-handler > also considers list formats to be text when exchanging block formats', (t) => {
115 | const string = '> 1. Some text';
116 | const indices = [0, 14];
117 |
118 | const headingify = blockHandler(string, indices, '# ');
119 |
120 | t.equal(headingify.value, '# 1. Some text');
121 | t.deepEqual(headingify.range, [2, 14]);
122 | t.end();
123 | });
124 |
125 | test('block-handler > can deal with a blank mark', (t) => {
126 | const string = '# Some text';
127 | const indices = [0, 11];
128 |
129 | const headingify = blockHandler(string, indices, '');
130 |
131 | t.equal(headingify.value, 'Some text');
132 | t.deepEqual(headingify.range, [0, 9]);
133 | t.end();
134 | });
135 |
136 | test('block-handler > works on a blank line', (t) => {
137 | const string = '';
138 | const indices = [0, 0];
139 |
140 | const headingify = blockHandler(string, indices, '# ');
141 |
142 | t.equal(headingify.value, '# ');
143 | t.deepEqual(headingify.range, [2, 2]);
144 | t.end();
145 | });
146 |
147 | test('block-handler > removes other formatting on a blank line', (t) => {
148 | const string = '>';
149 | const indices = [0, 1];
150 |
151 | const headingify = blockHandler(string, indices, '# ');
152 |
153 | t.equal(headingify.value, '# ');
154 | t.deepEqual(headingify.range, [2, 2]);
155 | t.end();
156 | });
157 |
--------------------------------------------------------------------------------
/test/utils/indent-handler.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { indentHandler } from '../../src/utils/markdownHandlers';
3 |
4 | test('indent-handler > adds four spaces to the beginning of a line', (t) => {
5 | const string = 'Some text';
6 | const indices = [0, 9];
7 |
8 | const indentify = indentHandler(string, indices, 'in');
9 |
10 | t.equal(indentify.value, ' Some text');
11 | t.deepEqual(indentify.range, [0, 13]);
12 | t.end();
13 | });
14 |
15 | test('indent-handler > does not matter where the selection is on that line', (t) => {
16 | const string = 'Some text';
17 | const indices = [9, 9];
18 |
19 | const indentify = indentHandler(string, indices, 'in');
20 |
21 | t.equal(indentify.value, ' Some text');
22 | t.deepEqual(indentify.range, [0, 13]);
23 | t.end();
24 | });
25 |
26 | test('indent-handler > works with multi-line selections', (t) => {
27 | const string = 'Some text\r\nSome other text';
28 | const indices = [0, 26];
29 |
30 | const indentify = indentHandler(string, indices, 'in');
31 |
32 | t.equal(indentify.value, ' Some text\r\n Some other text');
33 | t.deepEqual(indentify.range, [0, 33]);
34 | t.end();
35 | });
36 |
37 | test('indent-handler > ignores other lines around the selection', (t) => {
38 | const string = 'Some text\r\nSome other text';
39 | const indices = [11, 26];
40 | const indentify = indentHandler(string, indices, 'in');
41 |
42 | t.equal(indentify.value, 'Some text\r\n Some other text');
43 | t.deepEqual(indentify.range, [11, 30]);
44 | t.end();
45 | });
46 |
47 | test('indent-handler > does not remove block or list formatting', (t) => {
48 | const string = '- Some text';
49 | const indices = [0, 11];
50 |
51 | const indentify = indentHandler(string, indices, 'in');
52 |
53 | t.equal(indentify.value, ' - Some text');
54 | t.deepEqual(indentify.range, [0, 15]);
55 | t.end();
56 | });
57 |
58 | test('indent-handler > does not remove block or list formatting on outdent', (t) => {
59 | const string = ' - Some text';
60 | const indices = [0, 15];
61 |
62 | const indentify = indentHandler(string, indices, 'out');
63 |
64 | t.equal(indentify.value, '- Some text');
65 | t.deepEqual(indentify.range, [0, 11]);
66 | t.end();
67 | });
68 |
69 | test('indent-handler > considers inline formats to be text', (t) => {
70 | const string = '**Some text**';
71 | const indices = [0, 13];
72 |
73 | const indentify = indentHandler(string, indices, 'in');
74 |
75 | t.equal(indentify.value, ' **Some text**');
76 | t.deepEqual(indentify.range, [0, 17]);
77 | t.end();
78 | });
79 |
80 | test('indent-handler > also considers inline formats to be text on outdent', (t) => {
81 | const string = ' **Some text**';
82 | const indices = [0, 17];
83 |
84 | const indentify = indentHandler(string, indices, 'out');
85 |
86 | t.equal(indentify.value, '**Some text**');
87 | t.deepEqual(indentify.range, [0, 13]);
88 | t.end();
89 | });
90 |
91 | test('indent-handler > works on a blank line', (t) => {
92 | const string = '';
93 | const indices = [0, 0];
94 |
95 | const indentify = indentHandler(string, indices, 'in');
96 |
97 | t.equal(indentify.value, ' ');
98 | t.deepEqual(indentify.range, [0, 4]);
99 | t.end();
100 | });
101 |
102 | test('indent-handler > outdents on lines with multiple indents', (t) => {
103 | const string = ' Some text';
104 | const indices = [0, 17];
105 |
106 | const indentify = indentHandler(string, indices, 'out');
107 |
108 | t.equal(indentify.value, ' Some text');
109 | t.deepEqual(indentify.range, [0, 13]);
110 | t.end();
111 | });
112 |
113 | test('indent-handler > outdents on lines with less than a full indent', (t) => {
114 | const string = ' Some text';
115 | const indices = [0, 10];
116 |
117 | const indentify = indentHandler(string, indices, 'out');
118 |
119 | t.equal(indentify.value, 'Some text');
120 | t.deepEqual(indentify.range, [0, 9]);
121 | t.end();
122 | });
123 |
124 | test('indent-handler > outdents on lines with less than a full indent and an unordered list format string', (t) => {
125 | const string = ' - Some text';
126 | const indices = [0, 12];
127 |
128 | const indentify = indentHandler(string, indices, 'out');
129 |
130 | t.equal(indentify.value, '- Some text');
131 | t.deepEqual(indentify.range, [0, 11]);
132 | t.end();
133 | });
134 |
135 | test('indent-handler > outdents on lines with less than a full indent and an ordered list format string', (t) => {
136 | const string = ' 1. Some text';
137 | const indices = [0, 13];
138 |
139 | const indentify = indentHandler(string, indices, 'out');
140 |
141 | t.equal(indentify.value, '1. Some text');
142 | t.deepEqual(indentify.range, [0, 12]);
143 | t.end();
144 | });
145 |
146 | test('indent-handler > indents several lines', (t) => {
147 | const string = '- Some text\r\n- Some other text\r\n- Even more text';
148 | const indices = [0, 48];
149 |
150 | const indentify = indentHandler(string, indices, 'in');
151 |
152 | t.equal(indentify.value, ' - Some text\r\n - Some other text\r\n - Even more text');
153 | t.deepEqual(indentify.range, [0, 58]);
154 | t.end();
155 | });
156 |
157 | test('indent-handler > outdents several lines', (t) => {
158 | const string = ' 1. Some text\r\n 2. Some other text\r\n 3. Even more text';
159 | const indices = [0, 63];
160 |
161 | const indentify = indentHandler(string, indices, 'out');
162 |
163 | t.equal(indentify.value, '1. Some text\r\n2. Some other text\r\n3. Even more text');
164 | t.deepEqual(indentify.range, [0, 49]);
165 | t.end();
166 | });
167 |
168 | test('indent-handler > outdents several lines with less than a full indent on each line', (t) => {
169 | const string = ' 1. Some text\r\n 2. Some other text\r\n 3. Even more text';
170 | const indices = [0, 57];
171 |
172 | const indentify = indentHandler(string, indices, 'out');
173 |
174 | t.equal(indentify.value, '1. Some text\r\n2. Some other text\r\n3. Even more text');
175 | t.deepEqual(indentify.range, [0, 49]);
176 | t.end();
177 | });
178 |
179 | test('indent-handler > outdents several lines with a mix of indents', (t) => {
180 | const string = ' 1. Some text\r\n 2. Some other text\r\n 3. Even more text';
181 | const indices = [0, 60];
182 |
183 | const indentify = indentHandler(string, indices, 'out');
184 |
185 | t.equal(indentify.value, '1. Some text\r\n2. Some other text\r\n3. Even more text');
186 | t.deepEqual(indentify.range, [0, 49]);
187 | t.end();
188 | });
189 |
--------------------------------------------------------------------------------
/test/utils/inline-handler.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { inlineHandler } from '../../src/utils/markdownHandlers';
3 |
4 | test('inline-handler > adds a formatting string around a selection', (t) => {
5 | const string = 'Some text';
6 | const indices = [0, 9];
7 |
8 | const boldify = inlineHandler(string, indices, '**');
9 |
10 | t.equal(boldify.value, '**Some text**');
11 | t.deepEqual(boldify.range, [2, 11]);
12 | t.end();
13 | });
14 |
15 | test('inline-handler > removes a formatting string around a selection if it already has it', (t) => {
16 | const string = '**Some text**';
17 | const indices = [2, 11];
18 |
19 | const boldify = inlineHandler(string, indices, '**');
20 |
21 | t.equal(boldify.value, 'Some text');
22 | t.deepEqual(boldify.range, [0, 9]);
23 | t.end();
24 | });
25 |
26 | test('inline-handler > removes a formatting string inside a selection if it already has it', (t) => {
27 | const string = '~~Some text~~';
28 | const indices = [0, 13];
29 |
30 | const strikitize = inlineHandler(string, indices, '~~');
31 |
32 | t.equal(strikitize.value, 'Some text');
33 | t.deepEqual(strikitize.range, [0, 9]);
34 | t.end();
35 | });
36 |
37 | test('inline-handler > ignores other formatting strings', (t) => {
38 | const string = '~~Some text~~';
39 | const indices = [2, 11];
40 |
41 | const boldify = inlineHandler(string, indices, '**');
42 |
43 | t.equal(boldify.value, '~~**Some text**~~');
44 | t.deepEqual(boldify.range, [4, 13]);
45 | t.end();
46 | });
47 |
48 | test('inline-handler > ignores other formatting strings with removal', (t) => {
49 | const string = '~~**Some text**~~';
50 | const indices = [4, 13];
51 |
52 | const boldify = inlineHandler(string, indices, '**');
53 |
54 | t.equal(boldify.value, '~~Some text~~');
55 | t.deepEqual(boldify.range, [2, 11]);
56 | t.end();
57 | });
58 |
59 | test('inline-handler > can be used in the middle of ranges already marked', (t) => {
60 | const string = '**Some text**';
61 | const indices = [6, 13];
62 |
63 | const boldify = inlineHandler(string, indices, '**');
64 |
65 | t.equal(boldify.value, '**Some** text');
66 | t.deepEqual(boldify.range, [8, 13]);
67 | t.end();
68 | });
69 |
70 | test('inline-handler > sets selection range intuitively', (t) => {
71 | const string = '**Some text**';
72 | const indices = [6, 11];
73 |
74 | const boldify = inlineHandler(string, indices, '**');
75 |
76 | t.equal(boldify.value, '**Some** text');
77 | t.deepEqual(boldify.range, [8, 13]);
78 | t.end();
79 | });
80 |
81 | test('inline-handler > removes marks around blank strings', (t) => {
82 | const string = 'So****me text';
83 | const indices = [4, 4];
84 |
85 | const boldify = inlineHandler(string, indices, '**');
86 |
87 | t.equal(boldify.value, 'Some text');
88 | t.deepEqual(boldify.range, [2, 2]);
89 | t.end();
90 | });
91 |
--------------------------------------------------------------------------------
/test/utils/insert-handler.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { insertHandler } from '../../src/utils/markdownHandlers';
3 |
4 | test('insert-handler > inserts and selects the inserted markdown', (t) => {
5 | const string = 'Some text ';
6 | const indices = [10, 10];
7 |
8 | const boldify = insertHandler(string, indices, '[DISPLAY TEXT](https://url.com)');
9 |
10 | t.equal(boldify.value, 'Some text [DISPLAY TEXT](https://url.com)');
11 | t.deepEqual(boldify.range, [10, 41]);
12 | t.end();
13 | });
14 |
15 | test('insert-handler > inserts and selects the inserted markdown with a current selection', (t) => {
16 | const string = 'Some text ';
17 | const indices = [0, 10];
18 |
19 | const boldify = insertHandler(string, indices, '[DISPLAY TEXT](https://url.com)');
20 |
21 | t.equal(boldify.value, '[DISPLAY TEXT](https://url.com)');
22 | t.deepEqual(boldify.range, [0, 31]);
23 | t.end();
24 | });
25 |
--------------------------------------------------------------------------------
/test/utils/list-handler.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import { listHandler } from '../../src/utils/markdownHandlers';
3 |
4 | test('list-handler > adds a formatting string to the beginning of a line', (t) => {
5 | const string = 'Some text';
6 | const indices = [0, 9];
7 |
8 | const listify = listHandler(string, indices, 'ul');
9 |
10 | t.equal(listify.value, '- Some text');
11 | t.deepEqual(listify.range, [0, 11]);
12 | t.end();
13 | });
14 |
15 | test('list-handler > does not matter where the selection is on that line', (t) => {
16 | const string = 'Some text';
17 | const indices = [9, 9];
18 |
19 | const listify = listHandler(string, indices, 'ul');
20 |
21 | t.equal(listify.value, '- Some text');
22 | t.deepEqual(listify.range, [0, 11]);
23 | t.end();
24 | });
25 |
26 | test('list-handler > works with multi-line selections', (t) => {
27 | const string = 'Some text\r\nSome other text';
28 | const indices = [0, 26];
29 |
30 | const listify = listHandler(string, indices, 'ul');
31 |
32 | t.equal(listify.value, '- Some text\r\n- Some other text');
33 | t.deepEqual(listify.range, [0, 29]);
34 | t.end();
35 | });
36 |
37 | test('list-handler > ignores other lines around the selection', (t) => {
38 | const string = 'Some text\r\nSome other text';
39 | const indices = [11, 26];
40 | const listify = listHandler(string, indices, 'ul');
41 |
42 | t.equal(listify.value, 'Some text\r\n- Some other text');
43 | t.deepEqual(listify.range, [11, 28]);
44 | t.end();
45 | });
46 |
47 | test('list-handler > removes all other block or list formatting', (t) => {
48 | const string = '# Some text';
49 | const indices = [0, 11];
50 |
51 | const listify = listHandler(string, indices, 'ul');
52 |
53 | t.equal(listify.value, '- Some text');
54 | t.deepEqual(listify.range, [0, 11]);
55 | t.end();
56 | });
57 |
58 | test('list-handler > removes all other list formatting even if format string is directly touching text', (t) => {
59 | const string = '>Some text';
60 | const indices = [0, 10];
61 |
62 | const listify = listHandler(string, indices, 'ul');
63 |
64 | t.equal(listify.value, '- Some text');
65 | t.deepEqual(listify.range, [0, 11]);
66 | t.end();
67 | });
68 |
69 | test('list-handler > considers inline formats to be text', (t) => {
70 | const string = '**Some text**';
71 | const indices = [0, 13];
72 |
73 | const listify = listHandler(string, indices, 'ul');
74 |
75 | t.equal(listify.value, '- **Some text**');
76 | t.deepEqual(listify.range, [0, 15]);
77 | t.end();
78 | });
79 |
80 | test('list-handler > also considers inline formats to be text when removing other list formats', (t) => {
81 | const string = '> **Some text**';
82 | const indices = [0, 15];
83 |
84 | const listify = listHandler(string, indices, 'ul');
85 |
86 | t.equal(listify.value, '- **Some text**');
87 | t.deepEqual(listify.range, [0, 15]);
88 | t.end();
89 | });
90 |
91 | test('list-handler > works on a blank line', (t) => {
92 | const string = '';
93 | const indices = [0, 0];
94 |
95 | const listify = listHandler(string, indices, 'ul');
96 |
97 | t.equal(listify.value, '- ');
98 | t.deepEqual(listify.range, [0, 2]);
99 | t.end();
100 | });
101 |
102 | test('list-handler > removes other formatting on a blank line', (t) => {
103 | const string = '>';
104 | const indices = [0, 1];
105 |
106 | const listify = listHandler(string, indices, 'ul');
107 |
108 | t.equal(listify.value, '- ');
109 | t.deepEqual(listify.range, [0, 2]);
110 | t.end();
111 | });
112 |
113 | test('list-handler > adds formatting to several lines', (t) => {
114 | const string = 'Some text\r\nSome other text\r\nEven more text';
115 | const indices = [0, 42];
116 |
117 | const listify = listHandler(string, indices, 'ul');
118 |
119 | t.equal(listify.value, '- Some text\r\n- Some other text\r\n- Even more text');
120 | t.deepEqual(listify.range, [0, 46]);
121 | t.end();
122 | });
123 |
124 | test('list-handler > exchanges one unordered list for an ordered list', (t) => {
125 | const string = '- Some text\r\n- Some other text\r\n- Even more text';
126 | const indices = [0, 48];
127 |
128 | const listify = listHandler(string, indices, 'ol');
129 |
130 | t.equal(listify.value, '1. Some text\r\n2. Some other text\r\n3. Even more text');
131 | t.deepEqual(listify.range, [0, 49]);
132 | t.end();
133 | });
134 |
135 | test('list-handler > exchanges one ordered list for an unordered list', (t) => {
136 | const string = '1. Some text\r\n2. Some other text\r\n3. Even more text';
137 | const indices = [0, 51];
138 |
139 | const listify = listHandler(string, indices, 'ul');
140 |
141 | t.equal(listify.value, '- Some text\r\n- Some other text\r\n- Even more text');
142 | t.deepEqual(listify.range, [0, 46]);
143 | t.end();
144 | });
145 |
--------------------------------------------------------------------------------
/test/utils/parsers.spec.js:
--------------------------------------------------------------------------------
1 | import test from 'tape';
2 | import {
3 | indexOfMatch,
4 | indicesOfMatches,
5 | lastIndexOfMatch,
6 | splitLinesBackward,
7 | splitLines,
8 | startOfLine,
9 | endOfLine,
10 | } from '../../src/utils/parsers';
11 |
12 | test('parsers > creates a string prototype for getting indexOf a regex pattern', (t) => {
13 | const string = 'abc def hij klm nop';
14 | t.equal(indexOfMatch(string, /\s/g, 1), 3);
15 | t.end();
16 | });
17 |
18 | test('parsers > creates a string prototype for getting lastIndexOf a regex pattern', (t) => {
19 | const string = 'abc def hij klm nop';
20 | t.equal(lastIndexOfMatch(string, /\s/g, 5), 3);
21 | t.end();
22 | });
23 |
24 | test('parsers > creates a string prototype for getting indices of all regex matches', (t) => {
25 | const string = 'abc def hij klm nop';
26 | t.deepEqual(indicesOfMatches(string, /\s/g, 1), [3, 7, 11, 15]);
27 | t.end();
28 | });
29 |
30 | test('parsers > creates a string prototype for all line splits', (t) => {
31 | const string = 'abc\r\ndef\rhij\nklm nop';
32 | t.deepEqual(splitLines(string), ['abc', 'def', 'hij', 'klm nop']);
33 | t.end();
34 | });
35 |
36 | test('parsers > creates a string prototype for all line splits with a starting position', (t) => {
37 | const string = 'abc\r\ndef\rhij\nklm nop';
38 | t.deepEqual(splitLines(string, 10), ['ij', 'klm nop']);
39 | t.end();
40 | });
41 |
42 | test('parsers > creates a string prototype for all line splits with an ending position', (t) => {
43 | const string = 'abc\r\ndef\rhij\nklm nop';
44 | t.deepEqual(splitLinesBackward(string, 10), ['abc', 'def', 'h']);
45 | t.end();
46 | });
47 |
48 | test('parsers > finds the beginning of a line given a starting position', (t) => {
49 | const string = 'abc\r\ndef\rhij\nklm nop';
50 | t.equal(startOfLine(string, 10), 9);
51 | t.end();
52 | });
53 |
54 | test('parsers > finds the end of a line given a starting position', (t) => {
55 | const string = 'abc\r\ndef\rhij\nklm nop';
56 | t.equal(endOfLine(string, 10), 12);
57 | t.end();
58 | });
59 |
--------------------------------------------------------------------------------