Repository: tholman/zenpen Branch: master Commit: b39f57bbf505 Files: 11 Total size: 33.3 KB Directory structure: gitextract_4jowxg8a/ ├── .github/ │ └── FUNDING.yml ├── .gitignore ├── css/ │ ├── fonts.css │ └── style.css ├── index.html ├── js/ │ ├── default.js │ ├── editor.js │ ├── ui.js │ └── utils.js ├── license.md └── readme.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ github: [tholman] custom: ['https://www.buymeacoffee.com/tholman'] ================================================ FILE: .gitignore ================================================ .c9revisions .DS_Store ================================================ FILE: css/fonts.css ================================================ @font-face { font-family: 'icomoon'; src:url('fonts/icomoon.eot'); src:url('fonts/icomoon.eot?#iefix') format('embedded-opentype'), url('fonts/icomoon.woff') format('woff'), url('fonts/icomoon.ttf') format('truetype'), url('fonts/icomoon.svg#icomoon') format('svg'); font-weight: normal; font-style: normal; } /* Use the following CSS code if you want to use data attributes for inserting your icons */ [data-icon]:before { font-family: 'icomoon'; content: attr(data-icon); speak: none; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; } /* Use the following CSS code if you want to have a class per icon */ /* Instead of a list of all class selectors, you can use the generic selector below, but it's slower: [class*="icon-"] { */ .icon-expand, .icon-target, .icon-contrast, .icon-floppy, .icon-contract, .icon-link, .icon-download-alt { font-family: 'icomoon'; speak: none; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; } .icon-expand:before { content: "\e000"; } .icon-target:before { content: "\e001"; } .icon-contrast:before { content: "\e002"; } .icon-floppy:before { content: "\e003"; } .icon-contract:before { content: "\e004"; } .icon-link:before { content: "\e005"; } .icon-download-alt:before { content: "\e006"; } ================================================ FILE: css/style.css ================================================ /*! * ZenPen * http://www.zenpen.io * MIT licensed * * Copyright (C) Tim Holman, http://tholman.com */ /********************************************* * BASE STYLES *********************************************/ * { -moz-box-sizing: border-box; box-sizing: border-box; } *:focus { outline: none; } html { overflow: hidden; } html, body { font-family: 'Lora', serif; padding: 0; margin: 0; height: 100%; } body { padding-bottom: 40px; padding-right: 10px; overflow-y: scroll; padding-left: 10px; padding-top: 20px; min-width: 800px; width: 100%; -webkit-transition: all 600ms; -moz-transition: all 600ms; -ms-transition: all 600ms; -o-transition: all 600ms; transition: all 600ms; } section { max-width: 600px; height: 100%; margin: auto; } header { font-weight: bold; font-size: 38px; word-wrap: break-word; } article { padding-bottom: 50px; line-height: 30px; margin-top: 22px; min-height: 90%; font-size: 22px; display: block; word-wrap: break-word; } blockquote { border-left: 4px solid deepskyblue; margin-left: -19px; padding-left: 15px; margin-right: 0; } .no-overflow { overflow: hidden; display: block; height: 100%; width: 100%; } /* Used by the ui bubble to stop wrapping */ .lengthen { display: block; width: 300px; height: 100%; } .useicons { -webkit-font-smoothing: antialiased; font-size: 20px !important; font-family: 'icomoon' !important; } .yin { background: #fdfdfd; color: #111; } .yang { background-color: #111; color: #fafafa; } .ui { position: fixed; padding: 20px; width: 65px; bottom: 0; left: 0; top: 0; } .ui:hover button, .ui:hover .about { opacity: .4; } .ui button:hover, .ui .about:hover { opacity: 1; } .ui button, .text-options button { -webkit-transition: opacity 400ms; -moz-transition: opacity 400ms; -ms-transition: opacity 400ms; -o-transition: opacity 400ms; transition: opacity 400ms; font-family: inherit; background: none; cursor: pointer; font-size: 25px; color: inherit; opacity: .1; padding: 0; height: 32px; width: 25px; border: 0; } a { text-decoration: none; color: deepskyblue; } a:hover { text-decoration: underline; } .overlay { position: fixed; display: none; height: 100%; width: 100%; z-index: 3; left: 0; top: 0; } .quote { line-height: 60px !important; font-size: 49px !important; } /********************************************* * MODAL *********************************************/ .yang .modal { background-color: rgba(255,255,255,.9); color: #111; } .modal { background-color: rgba(0,0,0,.9); margin-left: -200px; position: absolute; border-radius: 3px; height: 101px; padding: 15px; display: none; width: 400px; bottom: 10px; color: #fff; left: 50%; } .modal h1 { text-align: center; font-size: 20px; padding: 0; margin: 0; } .modal div { margin-bottom: 10px; margin-top: 10px; } .modal input[type="number"] { font-size: 16px; display: block; margin: auto; width: 150px; padding: 5px; } .description { height: auto; } .description p { margin-bottom: 0; text-align: center; } .saveoverlay { margin-left: -215px; margin-top: -100px; height: 170px; left: 50%; top: 50%; } .saveoverlay div { text-align: center; font-size: 11px; } .saveselection { margin-top: 17px; text-align:center; } .saveselection span { -webkit-transition: color 250ms, background 250ms; -moz-transition: color 250ms, background 250ms; -ms-transition: color 250ms, background 250ms; -o-transition: color 250ms, background 250ms; transition: color 250ms, background 250ms; cursor: pointer; font-size: 15px; margin: 5px; padding: 5px; border: 2px solid white; border-radius: 3px; } .saveselection span:hover { background: rgba(255,255,255,.8); color: black; } .savebutton { -webkit-transition: opacity 250ms; -moz-transition: opacity 250ms; -ms-transition: opacity 250ms; -o-transition: opacity 250ms; transition: opacity 250ms; font-size: 30px !important; margin: 15px auto; background: none; cursor: pointer; display: block; border: none; padding: 0; width: 80px; color: #fff; margin-top: -2px; } .yang .savebutton { color: #000; } .savebutton:hover { opacity: .7; } .activesave { background: rgba(255,255,255,.8); color: black; } .hiddentextbox { opacity:0; filter:alpha(opacity=0); position:absolute; } /********************************************* * WORD COUNT *********************************************/ .wordcount { margin-left: -150px; width: 300px; } .word-counter { box-shadow: inset 0 0 9px -2px rgba(0,0,0,.9); position: fixed; height: 100%; right: -6px; width: 6px; top: 0; } .word-counter.active { right: 0; } .word-counter .progress { -webkit-transition: all 400ms ease-in-out; -moz-transition: all 400ms ease-in-out; -ms-transition: all 400ms ease-in-out; -o-transition: all 400ms ease-in-out; transition: all 400ms ease-in-out; background-color: deepskyblue; position: absolute; bottom: 0; width: 100%; height: 0%; } .progress.complete{ background-color: greenyellow; } /********************************************* * UI BUBBLE *********************************************/ .text-options { -webkit-transition: opacity 250ms, margin 250ms; -moz-transition: opacity 250ms, margin 250ms; -ms-transition: opacity 250ms, margin 250ms; -o-transition: opacity 250ms, margin 250ms; transition: opacity 250ms, margin 250ms; position: absolute; left: -999px; top: -999px; color: #fff; height: 0; width: 0; z-index: 5; margin-top: 5px; opacity: 0; } .text-options.fade { opacity: 0; margin-top: -5px; } .text-options.active { opacity: 1; margin-top: 0; } .options { background-color: rgba(0,0,0,.9); position: absolute; border-radius: 5px; margin-left: -63px; margin-top: -46px; z-index: 1000; padding: 5px 4px 5px 5px; width: 125px; height: 40px; -webkit-transition: all 300ms ease-in-out; -moz-transition: all 300ms ease-in-out; -ms-transition: all 300ms ease-in-out; -o-transition: all 300ms ease-in-out; transition: all 300ms ease-in-out; } .options.url-mode { width: 275px; margin-left: -137px; } .options.url-mode .bold, .options.url-mode .italic, .options.url-mode .quote { width: 0; overflow: hidden; margin-right: 0; opacity: 0; } .options .italic { font-style: italic; } .options button { transition: all 250ms ease-in-out; float: left; width: 28px; opacity: .7; height: 30px; border-radius: 3px; margin-right: 1px; font-family: 'Lora', serif; } .about { opacity: 0.4; transition: opacity 250ms ease-in-out; } .options.url-mode input{ border-left: 2px solid transparent; padding-right: 5px; padding-left: 5px; width: 236px; } .options input { -webkit-transition: all 300ms ease-in-out; -moz-transition: all 300ms ease-in-out; -ms-transition: all 300ms ease-in-out; -o-transition: all 300ms ease-in-out; transition: all 300ms ease-in-out; border-radius: 3px; overflow: hidden; outline: 0; height: 30px; padding: 0; margin: 0; border: 0; float: left; width: 0; } .options button.active { background-color: rgba(255,255,255,.4); opacity: 1; } .yang .options button.active { background-color: rgba(0,0,0,.3); } .options button:hover, .about:hover { opacity: .95; } .options:before { content: ""; border-top: 5px solid rgba(0,0,0,.9); border-bottom: 5px solid transparent; border-right: 5px solid transparent; border-left: 5px solid transparent; position: absolute; margin-left: -5px; bottom: -15px; height: 5px; width: 0; left: 50%; } .yang .options { background-color: rgba(255,255,255,.9); color: #000; } .yang .options:before { border-top: 5px solid rgba(255,255,255,.9); } .url { -webkit-font-smoothing: antialiased; } .top { position: absolute; top: 0; } .bottom { position: absolute; bottom: 0; } .about { font-size: 28px !important; filter: grayscale(1); text-decoration: none !important; } .wrapper { position: relative; height: 100%; } /********************************************* * PRINT *********************************************/ @media print { body { overflow: visible; } section { color: #111 !important; } .text-options, .ui, .word-counter { display: none; } } ================================================ FILE: index.html ================================================ ZenPen ~ Minimal Distraction, Maximum Zen
================================================ FILE: js/default.js ================================================ var defaultTitle = 'This is ZenPen'; var defaultContent = '

\ A minimalist writing zone, where you can block out all distractions and get to what\'s important. The writing! \

\

\ To get started, all you need to do is delete this text (seriously, just highlight it and hit delete), and fill the page with your own fantastic words. You can even change the title! \

\

\ You can use bold, italics, both and urls just by highlighting the text and selecting them from the tiny options box that appears above it.\

\
\ Quotes are easy to add too!\
\

\ If you\'re using ZenPen, and want to contribute a few dollars, there\'s a small donate button on the bottom left.\

\

Happy Typing! ~ Tim Holman (@twholman)

'; ================================================ FILE: js/editor.js ================================================ // editor ZenPen = window.ZenPen || {}; ZenPen.editor = (function() { // Editor elements var headerField, contentField, lastType, currentNodeList, lastSelection; // Editor Bubble elements var textOptions, optionsBox, boldButton, italicButton, quoteButton, urlButton, urlInput; var composing; function init() { composing = false; bindElements(); createEventBindings(); // Load state if storage is supported if ( ZenPen.util.supportsHtmlStorage() ) { loadState(); } else { loadDefault(); } // Set cursor position var range = document.createRange(); var selection = window.getSelection(); range.setStart(headerField, 1); selection.removeAllRanges(); selection.addRange(range); } function createEventBindings() { // Key up bindings if ( ZenPen.util.supportsHtmlStorage() ) { document.onkeyup = function( event ) { checkTextHighlighting( event ); saveState(); } } else { document.onkeyup = checkTextHighlighting; } // Mouse bindings document.onmousedown = checkTextHighlighting; document.onmouseup = function( event ) { setTimeout( function() { checkTextHighlighting( event ); }, 1); }; // Window bindings window.addEventListener( 'resize', function( event ) { updateBubblePosition(); }); document.body.addEventListener( 'scroll', function() { // TODO: Debounce update bubble position to stop excessive redraws updateBubblePosition(); }); // Composition bindings. We need them to distinguish // IME composition from text selection document.addEventListener( 'compositionstart', onCompositionStart ); document.addEventListener( 'compositionend', onCompositionEnd ); } function bindElements() { headerField = document.querySelector( '.header' ); contentField = document.querySelector( '.content' ); textOptions = document.querySelector( '.text-options' ); optionsBox = textOptions.querySelector( '.options' ); boldButton = textOptions.querySelector( '.bold' ); boldButton.onclick = onBoldClick; italicButton = textOptions.querySelector( '.italic' ); italicButton.onclick = onItalicClick; quoteButton = textOptions.querySelector( '.quote' ); quoteButton.onclick = onQuoteClick; urlButton = textOptions.querySelector( '.url' ); urlButton.onmousedown = onUrlClick; urlInput = textOptions.querySelector( '.url-input' ); urlInput.onblur = onUrlInputBlur; urlInput.onkeydown = onUrlInputKeyDown; } function checkTextHighlighting( event ) { var selection = window.getSelection(); if ( (event.target.className === "url-input" || event.target.classList.contains( "url" ) || event.target.parentNode.classList.contains( "ui-inputs" ) ) ) { currentNodeList = findNodes( selection.focusNode ); updateBubbleStates(); return; } // Check selections exist if ( selection.isCollapsed === true && lastType === false ) { onSelectorBlur(); } // Text is selected if ( selection.isCollapsed === false && composing === false ) { currentNodeList = findNodes( selection.focusNode ); // Find if highlighting is in the editable area if ( hasNode( currentNodeList, "ARTICLE") ) { updateBubbleStates(); updateBubblePosition(); // Show the ui bubble textOptions.className = "text-options active"; } } lastType = selection.isCollapsed; } function updateBubblePosition() { var selection = window.getSelection(); var range = selection.getRangeAt(0); var boundary = range.getBoundingClientRect(); textOptions.style.top = boundary.top - 5 + window.pageYOffset + "px"; textOptions.style.left = (boundary.left + boundary.right)/2 + "px"; } function updateBubbleStates() { // It would be possible to use classList here, but I feel that the // browser support isn't quite there, and this functionality doesn't // warrent a shim. if ( hasNode( currentNodeList, 'B') ) { boldButton.className = "bold active" } else { boldButton.className = "bold" } if ( hasNode( currentNodeList, 'I') ) { italicButton.className = "italic active" } else { italicButton.className = "italic" } if ( hasNode( currentNodeList, 'BLOCKQUOTE') ) { quoteButton.className = "quote active" } else { quoteButton.className = "quote" } if ( hasNode( currentNodeList, 'A') ) { urlButton.className = "url useicons active" } else { urlButton.className = "url useicons" } } function onSelectorBlur() { textOptions.className = "text-options fade"; setTimeout( function() { if (textOptions.className == "text-options fade") { textOptions.className = "text-options"; textOptions.style.top = '-999px'; textOptions.style.left = '-999px'; } }, 260 ) } function findNodes( element ) { var nodeNames = {}; // Internal node? var selection = window.getSelection(); // if( selection.containsNode( document.querySelector('b'), false ) ) { // nodeNames[ 'B' ] = true; // } while ( element.parentNode ) { nodeNames[element.nodeName] = true; element = element.parentNode; if ( element.nodeName === 'A' ) { nodeNames.url = element.href; } } return nodeNames; } function hasNode( nodeList, name ) { return !!nodeList[ name ]; } function saveState( event ) { localStorage[ 'header' ] = headerField.innerHTML; localStorage[ 'content' ] = contentField.innerHTML; } function loadState() { if ( localStorage[ 'header' ] ) { headerField.innerHTML = localStorage[ 'header' ]; } else { headerField.innerHTML = defaultTitle; // in default.js } if ( localStorage[ 'content' ] ) { contentField.innerHTML = localStorage[ 'content' ]; } else { loadDefaultContent() } } function loadDefault() { headerField.innerHTML = defaultTitle; // in default.js loadDefaultContent(); } function loadDefaultContent() { contentField.innerHTML = defaultContent; // in default.js } function onBoldClick() { document.execCommand( 'bold', false ); } function onItalicClick() { document.execCommand( 'italic', false ); } function onQuoteClick() { var nodeNames = findNodes( window.getSelection().focusNode ); if ( hasNode( nodeNames, 'BLOCKQUOTE' ) ) { document.execCommand( 'formatBlock', false, 'p' ); document.execCommand( 'outdent' ); } else { document.execCommand( 'formatBlock', false, 'blockquote' ); } } function onUrlClick() { if ( optionsBox.className == 'options' ) { optionsBox.className = 'options url-mode'; // Set timeout here to debounce the focus action setTimeout( function() { var nodeNames = findNodes( window.getSelection().focusNode ); if ( hasNode( nodeNames , "A" ) ) { urlInput.value = nodeNames.url; } else { // Symbolize text turning into a link, which is temporary, and will never be seen. document.execCommand( 'createLink', false, '/' ); } // Since typing in the input box kills the highlighted text we need // to save this selection, to add the url link if it is provided. lastSelection = window.getSelection().getRangeAt(0); lastType = false; urlInput.focus(); }, 100); } else { optionsBox.className = 'options'; } } function onUrlInputKeyDown( event ) { if ( event.keyCode === 13 ) { event.preventDefault(); applyURL( urlInput.value ); urlInput.blur(); } } function onUrlInputBlur( event ) { optionsBox.className = 'options'; applyURL( urlInput.value ); urlInput.value = ''; currentNodeList = findNodes( window.getSelection().focusNode ); updateBubbleStates(); } function applyURL( url ) { rehighlightLastSelection(); // Unlink any current links document.execCommand( 'unlink', false ); if (url !== "") { // Insert HTTP if it doesn't exist. if ( !url.match("^(http|https)://") ) { url = "http://" + url; } document.execCommand( 'createLink', false, url ); } } function rehighlightLastSelection() { var selection = window.getSelection(); if (selection.rangeCount > 0) { selection.removeAllRanges(); } selection.addRange( lastSelection ); } function getWordCount() { var text = ZenPen.util.getText( contentField ); if ( text === "" ) { return 0 } else { return text.split(/\s+/).length; } } function onCompositionStart ( event ) { composing = true; } function onCompositionEnd (event) { composing = false; } return { init: init, saveState: saveState, getWordCount: getWordCount } })(); ================================================ FILE: js/ui.js ================================================ // ui functions ZenPen = window.ZenPen || {}; ZenPen.ui = (function() { // Base elements var body, article, uiContainer, overlay, header; // Buttons var screenSizeElement, colorLayoutElement, targetElement, saveElement; // Word Counter var wordCountValue, wordCountBox, wordCountElement, wordCounter, wordCounterProgress; //save support var supportsSave, saveFormat, textToWrite; var expandScreenIcon = ''; var shrinkScreenIcon = ''; var darkLayout = false; function init() { supportsSave = !!new Blob()?true:false; bindElements(); wordCountActive = false; if ( ZenPen.util.supportsHtmlStorage() ) { loadState(); } console.log( "Checkin under the hood eh? We've probably got a lot in common. You should totally check out ZenPen on github! (https://github.com/tholman/zenpen)." ); } function loadState() { // Activate word counter if ( localStorage['wordCount'] && localStorage['wordCount'] !== "0") { wordCountValue = parseInt(localStorage['wordCount']); wordCountElement.value = localStorage['wordCount']; wordCounter.className = "word-counter active"; updateWordCount(); } // Activate color switch if ( localStorage['darkLayout'] === 'true' ) { if ( darkLayout === false ) { document.body.className = 'yang'; } else { document.body.className = 'yin'; } darkLayout = !darkLayout; } } function saveState() { if ( ZenPen.util.supportsHtmlStorage() ) { localStorage[ 'darkLayout' ] = darkLayout; localStorage[ 'wordCount' ] = wordCountElement.value; } } function bindElements() { // Body element for light/dark styles body = document.body; uiContainer = document.querySelector( '.ui' ); // UI element for color flip colorLayoutElement = document.querySelector( '.color-flip' ); colorLayoutElement.onclick = onColorLayoutClick; // UI element for full screen screenSizeElement = document.querySelector( '.fullscreen' ); screenSizeElement.onclick = onScreenSizeClick; targetElement = document.querySelector( '.target '); targetElement.onclick = onTargetClick; //init event listeners only if browser can save if (supportsSave) { saveElement = document.querySelector( '.save' ); saveElement.onclick = onSaveClick; var formatSelectors = document.querySelectorAll( '.saveselection span' ); for( var i in formatSelectors ) { formatSelectors[i].onclick = selectFormat; } document.querySelector('.savebutton').onclick = saveText; } else { document.querySelector('.save.useicons').style.display = "none"; } // Overlay when modals are active overlay = document.querySelector( '.overlay' ); overlay.onclick = onOverlayClick; article = document.querySelector( '.content' ); article.onkeyup = onArticleKeyUp; wordCountBox = overlay.querySelector( '.wordcount' ); wordCountElement = wordCountBox.querySelector( 'input' ); wordCountElement.onchange = onWordCountChange; wordCountElement.onkeyup = onWordCountKeyUp; saveModal = overlay.querySelector('.saveoverlay'); wordCounter = document.querySelector( '.word-counter' ); wordCounterProgress = wordCounter.querySelector( '.progress' ); header = document.querySelector( '.header' ); header.onkeypress = onHeaderKeyPress; } function onScreenSizeClick( event ) { screenfull.toggle(); if ( screenfull.enabled ) { document.addEventListener( screenfull.raw.fullscreenchange, function () { if ( screenfull.isFullscreen ) { screenSizeElement.innerHTML = shrinkScreenIcon; } else { screenSizeElement.innerHTML = expandScreenIcon; } }); } }; function onColorLayoutClick( event ) { if ( darkLayout === false ) { document.body.className = 'yang'; } else { document.body.className = 'yin'; } darkLayout = !darkLayout; saveState(); } function onTargetClick( event ) { overlay.style.display = "block"; wordCountBox.style.display = "block"; wordCountElement.focus(); } function onSaveClick( event ) { overlay.style.display = "block"; saveModal.style.display = "block"; } function saveText( event ) { if (typeof saveFormat != 'undefined' && saveFormat != '') { var blob = new Blob([textToWrite], {type: "text/plain;charset=utf-8"}); /* remove tabs and line breaks from header */ var headerText = header.innerHTML.replace(/(\t|\n|\r)/gm,""); if (headerText === "") { headerText = "ZenPen"; } saveAs(blob, headerText + '.txt'); } else { document.querySelector('.saveoverlay h1').style.color = '#FC1E1E'; } } /* Allows the user to press enter to tab from the title */ function onHeaderKeyPress( event ) { if ( event.keyCode === 13 ) { event.preventDefault(); article.focus(); } } /* Allows the user to press enter to tab from the word count modal */ function onWordCountKeyUp( event ) { if ( event.keyCode === 13 ) { event.preventDefault(); setWordCount( parseInt(this.value) ); removeOverlay(); article.focus(); } } function onWordCountChange( event ) { setWordCount( parseInt(this.value) ); } function setWordCount( count ) { // Set wordcount ui to active if ( count > 0) { wordCountValue = count; wordCounter.className = "word-counter active"; updateWordCount(); } else { wordCountValue = 0; wordCounter.className = "word-counter"; } saveState(); } function onArticleKeyUp( event ) { if ( wordCountValue > 0 ) { updateWordCount(); } } function updateWordCount() { var wordCount = ZenPen.editor.getWordCount(); var percentageComplete = wordCount / wordCountValue; wordCounterProgress.style.height = percentageComplete * 100 + '%'; if ( percentageComplete >= 1 ) { wordCounterProgress.className = "progress complete"; } else { wordCounterProgress.className = "progress"; } } function selectFormat( e ) { if ( document.querySelectorAll('span.activesave').length > 0 ) { document.querySelector('span.activesave').className = ''; } document.querySelector('.saveoverlay h1').style.cssText = ''; var targ; if (!e) var e = window.event; if (e.target) targ = e.target; else if (e.srcElement) targ = e.srcElement; // defeat Safari bug if (targ.nodeType == 3) { targ = targ.parentNode; } targ.className ='activesave'; saveFormat = targ.getAttribute('data-format'); var header = document.querySelector('header.header'); var headerText = header.innerHTML.replace(/(\r\n|\n|\r)/gm,"") + "\n"; var body = document.querySelector('article.content'); var bodyText = body.innerHTML; textToWrite = formatText(saveFormat,headerText,bodyText); var textArea = document.querySelector('.hiddentextbox'); textArea.value = textToWrite; textArea.focus(); textArea.select(); } function formatText( type, header, body ) { var text; switch( type ) { case 'html': header = "

" + header + "

"; text = header + body; text = text.replace(/\t/g, ''); break; case 'markdown': header = header.replace(/\t/g, ''); header = header.replace(/\n$/, ''); header = "#" + header + "#"; text = body.replace(/\t/g, ''); text = text.replace(/|<\/b>/g,"**") .replace(/\r\n+|\r+|\n+|\t+/ig,"") .replace(/|<\/i>/g,"_") .replace(/
/g,"> ") .replace(/<\/blockquote>/g,"") .replace(/

|<\/p>/gi,"\n") .replace(/
/g,"\n"); var links = text.match(/(.+)<\/a>/gi); if (links !== null) { for ( var i = 0; i 0) { document.querySelector('span.activesave').className = ''; } saveFormat=''; } return { init: init } })(); ================================================ FILE: js/utils.js ================================================ // Utility functions ZenPen = window.ZenPen || {}; ZenPen.util = (function() { function supportsHtmlStorage() { try { return 'localStorage' in window && window['localStorage'] !== null; } catch (e) { return false; } }; function getText(el) { var ret = " "; var length = el.childNodes.length; for(var i = 0; i < length; i++) { var node = el.childNodes[i]; if(node.nodeType != 8) { if ( node.nodeType != 1 ) { // Strip white space. ret += node.nodeValue; } else { ret += getText( node ); } } } return ZenPen.util.trim(ret); }; function trim(string) { return string.replace(/^\s+|\s+$/g, ''); }; return { trim: trim, getText: getText, supportsHtmlStorage: supportsHtmlStorage } })() ================================================ FILE: license.md ================================================ ## The MIT License (MIT) Copyright (c) 2025 ~ Tim Holman - @twholman Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: readme.md ================================================ ## ZenPen - A minimalist writing zone. Zenpen (http://zenpen.io) is a web app for writing minimally, and getting into the Zone. All information is persistent locally, using HTML5 local storage. ### ZenPen's minimal interface ![ZenPen](https://i.imgur.com/uP8Ensx.png) ### Text styling ![Text Styling](https://i.imgur.com/J8T88O7.png) ### Saving ![Saving](https://i.imgur.com/TkXX4aI.png) ### License The MIT License Copyright (C) 2016 ~ [Tim Holman](http://tholman.com) ~ timothy.w.holman@gmail.com