EpicEditor is an embeddable JavaScript Markdown editor with split fullscreen editing, live previewing, automatic draft saving, offline support, and more. For developers, it offers a robust API, can be easily themed, and allows you to swap out the bundled Markdown parser with anything you throw at it.

Related tags

Editors EpicEditor
Overview

⚠️ DEPRECATION WARNING

This repository is no longer actively maintained.

EpicEditor

An Embeddable JavaScript Markdown Editor

EpicEditor is an embeddable JavaScript Markdown editor with split fullscreen editing, live previewing, automatic draft saving, offline support, and more. For developers, it offers a robust API, can be easily themed, and allows you to swap out the bundled Markdown parser with anything you throw at it.

Why

Because, WYSIWYGs suck. Markdown is quickly becoming the replacement. GitHub, Stackoverflow, and even blogging apps like Posterous are now supporting Markdown. EpicEditor allows you to create a Markdown editor with a single line of JavaScript:

var editor = new EpicEditor().load();

Quick Start

EpicEditor is easy to implement. Add the script and assets to your page, provide a target container and call load().

Step 1: Download

Download the latest release or clone the repo:

$ git clone [email protected]:OscarGodson/EpicEditor

Step 2: Install

Copy EpicEditor/epiceditor/ onto your webserver, for example to /static/lib/epiceditor.

$ scp -r EpicEditor/epiceditor you@webserver:public_html/static/lib/

You can of course customize this step for your directory layout.

Step 3: Create your container element

<div id="epiceditor"></div>

Alternately, wrap an existing textarea to load the contents into the EpicEditor instance.

<div id="epiceditor"><textarea id="my-edit-area"></textarea></div>

Step 4: Add the epiceditor.js file

<script src="/static/lib/epiceditor/js/epiceditor.min.js"></script>

Step 5: Init EpicEditor

EpicEditor needs to know where to find its themes, so it needs to be told its install directory at init.

var editor = new EpicEditor({basePath: '/static/lib/epiceditor'}).load();

API

EpicEditor([options])

The EpicEditor constructor creates a new editor instance. Customize the instance by passing the options parameter. The example below uses all options and their defaults:

var opts = {
  container: 'epiceditor',
  textarea: null,
  basePath: 'epiceditor',
  clientSideStorage: true,
  localStorageName: 'epiceditor',
  useNativeFullscreen: true,
  parser: marked,
  file: {
    name: 'epiceditor',
    defaultContent: '',
    autoSave: 100
  },
  theme: {
    base: '/themes/base/epiceditor.css',
    preview: '/themes/preview/preview-dark.css',
    editor: '/themes/editor/epic-dark.css'
  },
  button: {
    preview: true,
    fullscreen: true,
    bar: "auto"
  },
  focusOnLoad: false,
  shortcut: {
    modifier: 18,
    fullscreen: 70,
    preview: 80
  },
  string: {
    togglePreview: 'Toggle Preview Mode',
    toggleEdit: 'Toggle Edit Mode',
    toggleFullscreen: 'Enter Fullscreen'
  },
  autogrow: false
}
var editor = new EpicEditor(opts);

Options

Option Description Default
container The ID (string) or element (object) of the target container in which you want the editor to appear. epiceditor
textarea The ID (string) or element (object) of a textarea you would like to sync the editor's content with. On page load if there is content in the textarea, the editor will use that as its content.
basePath The base path of the directory containing the /themes. epiceditor
clientSideStorage Setting this to false will disable localStorage. true
localStorageName The name to use for the localStorage object. epiceditor
useNativeFullscreen Set to false to always use faux fullscreen (the same as what is used for unsupported browsers). true
parser [Marked](https://github.com/chjj/marked) is the only parser built into EpicEditor, but you can customize or toggle this by passing a parsing function to this option. For example:
parser: MyCustomParser.parse
marked
focusOnLoad If true, editor will focus on load. false
file.name If no file exists with this name a new one will be made, otherwise the existing will be opened. container ID
file.defaultContent The content to show if no content exists for a file. NOTE: if the textarea option is used, the textarea's value will take precedence over defaultContent.
file.autoSave How often to auto save the file in milliseconds. Set to false to turn it off. 100
theme.base The base styles such as the utility bar with the buttons. themes/base/epiceditor.css
theme.editor The theme for the editor which is the area you type into. themes/editor/epic-dark.css
theme.preview The theme for the previewer. themes/preview/github.css
button If set to false will remove all buttons. All buttons set to true.
button.preview If set to false will remove the preview button. true
button.fullscreen If set to false will remove the fullscreen button. true
button.bar If true or "show", any defined buttons will always be visible. If false or "hide", any defined buttons will never be visible. If "auto", buttons will usually be hidden, but shown if whenever the mouse is moved. "auto"
shortcut.modifier The key to hold while holding the other shortcut keys to trigger a key combo. 18 (alt key)
shortcut.fullscreen The shortcut to open fullscreen. 70 (f key)
shortcut.preview The shortcut to toggle the previewer. 80 (p key)
string.togglePreview The tooltip text that appears when hovering the preview icon. Toggle Preview Mode
string.toggleEdit The tooltip text that appears when hovering the edit icon. Toggle Edit Mode
string.toggleFullscreen The tooltip text that appears when hovering the fullscreen icon. Enter Fullscreen
autogrow Whether to autogrow EpicEditor to fit its contents. If autogrow is desired one can either specify true, meaning to use default autogrow settings, or an object to define custom settings false
autogrow.minHeight The minimum height (in pixels) that the editor should ever shrink to. This may also take a function that returns the desired minHeight if this is not a constant, or a falsey value if no minimum is desired 80
autogrow.maxHeight The maximum height (in pixels) that the editor should ever grow to. This may also take a function that returns the desired maxHeight if this is not a constant, or a falsey value if no maximum is desired false
autogrow.scroll Whether the page should scroll to keep the caret in the same vertical place while autogrowing (recommended for mobile in particular) true

load([callback])

Loads the editor by inserting it into the DOM by creating an iframe. Will trigger the load event, or you can provide a callback.

editor.load(function () {
  console.log("Editor loaded.")
});

unload([callback])

Unloads the editor by removing the iframe. Keeps any options and file contents so you can easily call .load() again. Will trigger the unload event, or you can provide a callback.

editor.unload(function () {
  console.log("Editor unloaded.")
});

getElement(element)

Grabs an editor element for easy DOM manipulation. See the Themes section below for more on the layout of EpicEditor elements.

  • container: The element given at setup in the options.
  • wrapper: The wrapping <div> containing the 2 editor and previewer iframes.
  • wrapperIframe: The iframe containing the wrapper element.
  • editor: The #document of the editor iframe (i.e. you could do editor.getElement('editor').body).
  • editorIframe: The iframe containing the editor element.
  • previewer: The #document of the previewer iframe (i.e. you could do editor.getElement('previewer').body).
  • previewerIframe: The iframe containing the previewer element.
someBtn.onclick = function () {
  console.log(editor.getElement('editor').body.innerHTML); // Returns the editor's content
}

is(state)

Returns a boolean for the requested state. Useful when you need to know if the editor is loaded yet for example. Below is a list of supported states:

  • loaded
  • unloaded
  • edit
  • preview
  • fullscreen
fullscreenBtn.onclick = function () {
  if (!editor.is('loaded')) { return; }
  editor.enterFullscreen();
}

open(filename)

Opens a client side storage file into the editor.

Note: This does not open files on your server or machine (yet). This simply looks in localStorage where EpicEditor stores drafts.

openFileBtn.onclick = function () {
  editor.open('some-file'); // Opens a file when the user clicks this button
}

importFile([filename],[content])

Imports a string of content into a client side storage file. If the file already exists, it will be overwritten. Useful if you want to inject a bunch of content via AJAX. Will also run .open() after import automatically.

Note: This does not import files on your server or machine (yet). This simply looks in localStorage where EpicEditor stores drafts.

importFileBtn.onclick = function () {
  editor.importFile('some-file',"#Imported markdown\nFancy, huh?"); //Imports a file when the user clicks this button
}

exportFile([filename],[type])

Returns the plain text of the client side storage file, or if given a type, will return the content in the specified type. If you leave both parameters null it will return the current document's content in plain text. The supported export file types are:

Note: This does not export files to your server or machine (yet). This simply looks in localStorage where EpicEditor stores drafts.

  • text (default)
  • html
  • json (includes metadata)
  • raw (warning: this is browser specific!)
syncWithServerBtn.onclick = function () {
  var theContent = editor.exportFile();
  saveToServerAjaxCall('/save', {data:theContent}, function () {
    console.log('Data was saved to the database.');
  });
}

rename(oldName, newName)

Renames a client side storage file.

Note: This does not rename files on your server or machine (yet). This simply looks in localStorage where EpicEditor stores drafts.

renameFileBtn.onclick = function () {
  var newName = prompt('What do you want to rename this file to?');
  editor.rename('old-filename.md', newName); //Prompts a user and renames a file on button click
}

save()

Manually saves a file to client side storage (localStorage by default). EpicEditor will save continuously every 100ms by default, but if you set autoSave in the options to false or to longer intervals it's useful to manually save.

Note: This does not save files to your server or machine (yet). This simply looks in localStorage where EpicEditor stores drafts.

saveFileBtn.onclick = function () {
  editor.save();
}

remove(name)

Deletes a client side storage file.

Note: This does not remove files from your server or machine (yet). This simply looks in localStorage where EpicEditor stores drafts.

removeFileBtn.onclick = function () {
  editor.remove('example.md');
}

getFiles([name], [excludeContent])

If no name is given it returns an object containing the names and metadata of all client side storage file objects. If a name is specified it will return just the metadata of that single file object. If excludeContent is true, it will remove the content from the returned object. This is useful when you just want a list of files or get some meta data. If excludeContent is false (default), it'll return a content property per file in plain text format.

Note: This does not get files from your server or machine (yet). This simply looks in localStorage where EpicEditor stores drafts.

var files = editor.getFiles();
for (x in files) {
  console.log('File: ' + x); //Returns the name of each file
};

on(event, handler)

Sets up an event handler (callback) for a specified event. For all event types, see the Events section below.

editor.on('unload', function () {
  console.log('Editor was removed');
});

emit(event)

Fires an event programatically. Similar to jQuery's .trigger()

editor.emit('unload'); // Triggers the handler provided in the "on" method above

removeListener(event, [handler])

Allows you to remove all listeners for an event, or just the specified one.

editor.removeListener('unload'); //The handler above would no longer fire

preview()

Puts the editor into preview mode.

previewBtn.onclick = function () {
  editor.preview();
}

edit()

Puts the editor into edit mode.

editBtn.onclick = function () {
  editor.edit();
}

focus()

Puts focus on the editor or previewer (whichever is visible). Works just like doing plain old JavaScript and input focus like someInput.focus(). The benefit of using this method however, is that it handles cross browser issues and also will focus on the visible view (edit or preview).

showEditorBtn.onclick = function () {
  editorWrapper.style.display = 'block'; // switch from being hidden from the user
  editor.focus(); // Focus and allow user to start editing right away
}

enterFullscreen([callback])

Puts the editor into fullscreen mode. A callback will be fired after the entering fullscreen animation completes. Some browsers will be nearly instant while others, mainly Chrome, take 750ms before this event is fired. If already in fullscreen, the callback will fire immediately.

Note: due to browser security restrictions, calling enterFullscreen programmatically like this will not trigger native fullscreen. Native fullscreen can only be triggered by a user interaction like mousedown or keyup.

enterFullscreenBtn.onclick = function () {
  editor.enterFullscreen(function () {
    console.log('Welcome to fullscreen mode!');
  });
}

exitFullscreen([callback])

Closes fullscreen mode. A callback will be fired after the exiting fullscreen animation completes. If already not in fullscreen, the callback will fire immediately.

exitFullscreenBtn.onclick = function () {
  editor.exitFullscreen(function () {
    console.log('Finished closing fullscreen!');
  });
}

reflow([type], [callback])

reflow() allows you to "reflow" the editor in it's container. For example, let's say you increased the height of your wrapping element and want the editor to resize too. You could call reflow and the editor will resize to fit. You can pass it one of two strings as the first parameter to constrain the reflow to either width or height.

It also provides you with a callback parameter if you'd like to do something after the resize is finished. The callback will return the new width and/or height in an object. Additionally, you can also listen for the reflow event. This will also give you back the new size.

Note: If you call reflow() or reflow('width') and you have a fluid width container EpicEditor will no longer be fluid because doing a reflow on the width sets an inline style on the editor.

// For an editor that takes up the whole browser window:
window.onresize = function () {
  editor.reflow();
}

// Constrain the reflow to just height:
someDiv.resizeHeightHandle = function () {
  editor.reflow('height');
}

// Same as the first example, but this has a callback
window.onresize = function () {
  editor.reflow(function (data) {
    console.log('width: ', data.width, ' ', 'height: ', data.height);
  });
}

Events

You can hook into specific events in EpicEditor with on() such as when a file is created, removed, or updated. Below is a complete list of currently supported events and their description.

Event Name Description
create Fires whenever a new file is created.
read Fires whenever a file is read.
update Fires whenever a file is updated.
remove Fires whenever a file is deleted.
load Fires when the editor loads via load().
unload Fires whenever the editor is unloaded via unload()
preview Fires whenever the previewer is opened (excluding fullscreen) via preview() or the preview button.
edit Fires whenever the editor is opened (excluding fullscreen) via edit() or the edit button.
fullscreenenter Fires whenever the editor opens in fullscreen via fullscreen() or the fullscreen button.
fullscreenexit Fires whenever the editor closes in fullscreen via fullscreen() or the fullscreen button.
save Fires whenever save() is called manually, or implicitly by ```importFile``` or ```open```.
autosave Fires whenever the autoSave interval fires, and the file contents have been updated since the last save.
open Fires whenever a file is opened or loads automatically by EpicEditor or when open() is called.
reflow Fires whenever reflow() is called. Will return the new dimensions in the callback. Will also fire every time there is a resize from autogrow.

Themes

Theming is easy in EpicEditor. There are three different <iframe>s which means styles wont leak between the "chrome" of EpicEditor, previewer, or editor. Each one is like it's own web page. In the themes directory you'll see base, preview, and editor. The base styles are for the "chrome" of the editor which contains elements such as the utility bar containing the icons. The editor is the styles for the contents of editor <iframe> and the preview styles are applied to the preview <iframe>.

The HTML of a generated editor (excluding contents) looks like this:

<div id="container">
  <iframe id="epiceditor-instance-id">
    <html>
      <head>
        <link type="text/css" id="" rel="stylesheet" href="epiceditor/themes/base/epiceditor.css" media="screen">
      </head>
      <body>
        <div id="epiceditor-wrapper">
          <iframe id="epiceditor-editor-frame">
            <html>
              <head>
                <link type="text/css" rel="stylesheet" href="epiceditor/themes/editor/epic-dark.css" media="screen">
              </head>
              <body contenteditable="true">
                <!-- raw content -->
              </body>
            </html>
          </iframe>
          <iframe id="epiceditor-previewer-frame">
            <html>
              <head>
                <link type="text/css" rel="stylesheet" href="epiceditor/themes/preview/github.css" media="screen">
              </head>
              <body>
                <div id="epiceditor-preview" class="epiceditor-preview">
                  <!-- rendered html -->
                </div>
              </body>
            </html>
          </iframe>
          <div id="epiceditor-utilbar">
            <span title="Toggle Preview Mode" class="epiceditor-toggle-btn epiceditor-toggle-preview-btn"></span>
            <span title="Enter Fullscreen" class="epiceditor-fullscreen-btn"></span>
          </div>
        </div>
      </body>
    </html>
  </iframe>
</div>

Unlike the "chrome" of base or editor, the theming of the preview is done by CSS class so that you can reuse EpicEditor's theme to make your rendered page match your previewed.

First, include your chosen theme on every page:

<link rel="stylesheet" href="/epiceditor/themes/preview/github.css">

(you may need to adjust the path)

Mark your rendered content with .epiceditor-preview:

<div id="my-content" class="epiceditor-preview"> ... </div>

Custom Parsers

EpicEditor is set up to allow you to use any parser that accepts and returns a string. This means you can use any flavor of Markdown, process Textile, or even create a simple HTML editor/previewer (parser: false). The possibilities are endless. Just make the parser available and pass its parsing function to the EpicEditor setting and you should be all set. You can output plain text or HTML. Here's an example of a parser that could remove "bad words" from the preview:

var editor = new EpicEditor({
  parser: function (str) {
    var blacklist = ['foo', 'bar', 'baz'];
    return str.split(' ').map(function (word) {
      // If the word exists, replace with asterisks
      if (blacklist.indexOf(word) > -1) {
        return '****'
      }
      return word;
    }).join(' ');
  }
}).load();

Here's a Wiki to HTML parser by Remy Sharp used with EpicEditor:

var editor = new EpicEditor({
  parser: function (str) {
    return str.wiki2html();
  }
}).load();

For even more customization and optimization you can replace the default built-in processor on build. Running jake build parser=path/to/parser.js will override the default Marked build and replace it with your custom script.

Support

If you're having any problems with EpicEditor feel free to open a new ticket. Go ahead and ask us anything and we'll try to help however we can. You can also see if there's someone available at the #epiceditor IRC channel on irc.freenode.net. If you need a little more help with implementing EpicEditor on your site we've teamed up with CodersClan to offer support:

Contributing

Contributions are greatly encouraged and appreciated. For more on ways to contribute please check the wiki: Contributing Guide.

Credits

EpicEditor relies on Marked to parse markdown and is brought to you in part by Oscar Godson and John Donahue. Special thanks to Adam Bickford for the bug fixes and being the QA for pull requests. Lastly, huge thanks to Sebastian Nitu for the amazing logo and doc styles.

Comments
  • Enter key now inserts `\n` instead of leaving it for browsers to handle them

    Enter key now inserts `\n` instead of leaving it for browsers to handle them

    This change is in preparation for issue #159. I think this code is ready for review. Coincidentally, this fixes: #162 and #164.

    You can see the initial implementation of #159 (TAB/indentation support) on my fork branch. It was branched off this pull request.


    Things to look out for:

    1. Public API (there are 3 new public API calls)
    2. Event handling
    3. Specs/Tests… Please review the tests. Testing insertText was a little tricky without way to get text from EE.

    There is an issue with Firefox at the moment, whilst the insertText works when run in browser, the spec/tests I wrote fail in Firefox. It doesn't seem to like the getSelection call. I am currently trying to look at why and how to fix it. If you could help me polish the tests, that would be super helpful – as this is a big pull request.

    I have tested this code in Chrome, Firefox, Safari, and IE9.

    opened by hongymagic 44
  • charset in iframe is not utf8

    charset in iframe is not utf8

    i create a epicEditor with default setting, and entered below text:

    empty

       content
    

    then i get the content from the editor by using:

    var data = editor.exportFile('myfile');
    

    when i post the data to my server, i saw

    "#empty\n\n  content"

    the spaces are incorrectly encoded

    Important Bug 0.2.0 QA'd Needs Tests 
    opened by davidshen84 29
  • Parser as option

    Parser as option

    A bunch of updates here so let me break it down. Also I realize I didn't get much done on tests specifically, but the good news is, it now passes the linter :) ... and I'll be cleaning that up more this week.

    1. Added linting for other JS: Now jake lint will check all of the JS files listed below. jake build, jake docs, and jake test only lint their associated files. I'm not sure if you would want a force option for each so that is currently only available for build.
      • src/editor.js
      • Jakefile.js (Note: jshint does not currently support files without ext so .js was added.)
      • .jshintrc (just because it's funny to lint a lint config)
      • docs/js/main.js
      • spec/spec.js
    2. Pulled Marked out of EE wrapper: The intro.js and tmp file concat is no longer necessary. The default EE build will still use Marked but an optional parser can be provided by the user as described in 3. The only real drawback of this is that it creates an additional global but this seems like a reasonable thing to accept in trade for the flexibility it offers.
    3. Added settings.parser: This option accepts any function that returns a string. If the user has included Showdown or some textile parser on the page, the parsing function can be passed into this option otherwise EE will look for Marked by default and if that is not available, use a generic function that returns an error message followed by the original content wrapped in <pre> tags (See example below.). Putting the parser on the settings object seems like a great option rather than extending the EE object as we had discussed. I am imagining an implementation where a user is allowed to select between textile or markdown to parse their content. As a part of the settings object, this parser could be toggled at any point for a given instance and still function (e.g. realize your content is textile?... just switch the parser).
    4. Added build option to use custom parser: Running jake build parser=path/to/parser.js will override the default Marked build and replace it with the custom script. You can play around with this as follows:

    Clone Showdown:

    $ git clone https://github.com/coreyti/showdown.git
    

    Build EE using Showdown:

    $ jake build parser=path/to/showdown/src/showdown.js
    

    Modify docs/js/main.js to point to the Showdown parser:

      var opts =
          { container: 'example-1'
          , file: { defaultContent: "#EpicEditor\nThis is some default content. Go ahead, _change me_. " }
          , focusOnLoad: true
          , parser: new Showdown.converter()
          }
    

    Whoops! That's not the rendering function.

    Now correct the parser:

      var opts =
        { container: 'example-1'
        , file: { defaultContent: "#EpicEditor\nThis is some default content. Go ahead, _change me_. " }
        , focusOnLoad: true
        , parser: new Showdown.converter().makeHtml
        }
    

    Good to go.

    Current Jake tasks

    jake lint           # Lint all js files  
    jake lint:editor    # Lint core EpicEditor: src/editor.js  
    jake lint:docs      # Lint doc related js: docs/js/main.js  
    jake lint:spec      # Lint test related js: spec/spec.js  
    jake lint:util      # Lint utility and config js files  
    jake build          # Build epiceditor.js and minify to epiceditor.min.js  
    jake build:force    # Force build epiceditor.js and epiceditor.min.js skipping pre-reqs  
    jake docs           # Build index.html from the README  
    jake test           # Test code against specs  
    jake package        # Build the package for distribution  
    jake clobber        # Remove the package  
    jake ascii          # Kick out some ascii
    
    opened by johnmdonahue 29
  • Mocha Port

    Mocha Port

    PR to get the conversation started.

    This is basically just the first pass on the mocha port. It definitely needs some QA and I'd like to know how performance is affected on other machines.

    A few things to note:

    1. You can run a specific test block by clicking its header. You'll get something like this: http://localhost:8000/test/tests.html?grep=^\.edit\(\), which is handy if you're only trying to test changes to a specific method. This is also why I changed from index.html to tests.html (as is the convention in mocha's own example) because grep won't work on an empty path: i.e. http://localhost:8000/test/?grep=^\.edit\(\)
    2. Some test have been removed as redundant - these are mainly tests that checked state before running a test that depended on it but the methods used there should be tested in their specific API block anyway.
    3. I reworded all "should" descriptions to hopefully better describe the test and fit the standard expect conventions.
    4. I used EE API calls where possible to hopefully increase performance and readability. This should be ok in most cases since they should be tested on their own regardless. (There are definitely still some slow tests that can be optimized though)
    5. All tests have been broken into their own method specific files. This is handy to help get a grasp on coverage and hopefully reduce redundancy and improve findability.
    6. I left the foounit test suite intact for now so that performance can be tested. I will do a quick rewrite of jake test task soon to just spin up a simple node server.

    For anyone interested in playing around, jake test will still work to spin up a server until foounit is removed or you can just spin up any local server in the EE repo root and then hit localhost:8000/test/tests.html in a browser, e.g.:

    $ python -mSimpleHTTPServer
    

    I'll work on the larger package.json/Jakefile refactor on a separate branch when I get some more time. For now I think this works and in fact having Mocha deps included in the repo will allow us to hit the tests live on GHPages as we can now: http://epiceditor.com/spec/runner.html

    Let me know any thoughts. :D

    opened by johnmdonahue 28
  • Autosave/save split

    Autosave/save split

    Tried to conform to your style as best I could. Hopefully I didn't miss anything. Not clear if this change merits a new test or not.

    Also slipped in a fix to your test code to make the linter stop yelling at me when I ran jake test

    opened by Gankra 25
  •   displays with Firefox and getFile .content

      displays with Firefox and getFile .content

    Playing around with 0.2.1 along with syncing with a <textarea> and syntax highlighting via highlight.js, saving content to a MongoDB database, I find that within code tags, any indentation via spaces is outputted as &nbsp; for each space except the last, that one outputs fine.

    I've tried breaking things a little by removing the &nbsp;'s from .replace() in lines 183, 1377 and 1380 of epiceditor.js, but I still see that on output even if I create a new document after the changes... Is really doing my head in. I've also tried looking at highlight.js but there's not even a mention of &nbsp; there that I can find with Sublime Text.

    This only happens in code tags, nowhere else... Any thoughts?

    Important Bug 0.2.1 QA'd Firefox 
    opened by zspecza 25
  • Add private method _getSelection

    Add private method _getSelection

    Adds private method _getSelection(iframe) as requested in this comment.

    var selection = _getSelection(self.editorIframe);
    

    Will return a native Selection object. There are a couple of things to note going forward, I'll keep them short here:

    1. To get caret position, we will need to query the selection object for text ranges. Remember the ugly offset calculation? This will be required to get the accurate caret position
    2. Other commonly tested paths such as: if (selection.rangeCount > 0) range = selection.getRangeAt(0) could also be extracted out as a private function. I will leave that out for now
    opened by hongymagic 21
  • Chrome and IE9-10 replacing all spaces with  

    Chrome and IE9-10 replacing all spaces with  

    I don't know what happened, but suddenly every instance of EpicEditor (master, develop, my own fork) I look at now has this bug:

    • Type in some text into epic editor
    • Refresh
    • Inspect DOM; All spaces are now &nbsp;

    This really screws the word-wrapping, making the content hideous, to say the least.

    @OscarGodson are you seeing this too?

    Chrome 28.0.1500.71 OSX 10.8.4

    Not reproducing in Safari/FF.

    Really hoping my Chrome install is broken or something.

    Bug IE9 QA'd IE10 Chrome 0.2.2 
    opened by Gankra 19
  • Strip out HTML on pasting

    Strip out HTML on pasting

    I'm constantly having to paste into some text editor, then into EpicEditor. This is super annoying. We should catch onpaste then strip all HTML out, then paste the text. Also, it's subtle sometimes so shit just looks broken and to the end user they wont know.

    Important Bug QA'd 0.2.2 
    opened by OscarGodson 19
  • Read content from a textarea?

    Read content from a textarea?

    The textarea sync works nicely, but I have one problem and I'm not sure if we can find a workaround.

    Let's say user X edits the description of a product, the form textarea will be synched and the content will be saved to the database when the user submits the form, great!

    Now let's say user Y also wants to edit that product description so he opens the edit page in another browser in another machine, hence his localStorage object is empty. Fortunately, as the content was previously saved in the database, the textarea is populated with the last edit from user X but it seems that the textarea content is not read when loading the editor, only the localStorage object. In consequence, user Y sees a blank editor that syncs with the textarea and erases all the previous content without poor user Y noticing anything.

    I am missing something? Thanks!

    Important 0.2.1 QA'd 
    opened by acasademont 16
  • Nicer download btn and section

    Nicer download btn and section

    Dear @johnmdonahue,

    Added a nicer download button and moved it to a new 'Download' section which in the future could contain multiple versions like stable, beta, and alphas.

    The only issue is that I added this to the README.md so that we could have the download section also in the README. You didn't wanna update more than one place which I totally understand. What if we we use @VERSION in the README also? Would that work? Then if that works we can swap the zipUrl var with simply the href of that download link and then everywhere is updated even in the README.

    I also moved around some of the main.js so that when we add sections it doesn't break stuff by using the IDs of the elements rather than counting them.

    Love, Oscar :heart:

    opened by OscarGodson 16
  • Bump mime from 1.2.7 to 2.4.4

    Bump mime from 1.2.7 to 2.4.4

    Bumps mime from 1.2.7 to 2.4.4.

    Release notes

    Sourced from mime's releases.

    v2.0.3

    • Fix RegEx DoS issue

    v2.0.1

    • [closed] MIME breaking with 'class' declaration as it is without 'use strict mode' #170

    v2.0.0

    Version 2 is a breaking change from 1.x, as the semver implies. Specifically:

    • ES6 support required (node@>=6)
    • lookup() renamed to getType()
    • extension() renamed to getExtension()
    • charset() and load() methods have been removed
    • [closed] woff and woff2 #168

    v1.4.1

    • Fix RegEx DoS issue

    v1.4.0

    • [closed] support for ac3 voc files #159
    • [closed] Help understanding change from application/xml to text/xml #158
    • [closed] no longer able to override mimetype #157
    • [closed] application/vnd.adobe.photoshop #147
    • [closed] Directories should appear as something other than application/octet-stream #135
    • [closed] requested features #131
    • [closed] Make types.json loading optional? #129
    • [closed] Cannot find module './types.json' #120
    • [V2] .wav files show up as "audio/x-wav" instead of "audio/x-wave" #118
    • [closed] Don't be a pain in the ass for node community #108
    • [closed] don't make default_type global #78
    • [closed] mime.extension() fails if the content-type is parameterized #74
    Changelog

    Sourced from mime's changelog.

    2.4.4 (2019-06-07)

    2.4.3 (2019-05-15)

    2.4.2 (2019-04-07)

    Bug Fixes

    • don't use arrow function introduced in 2.4.1 (2e00b5c)

    2.4.1 (2019-04-03)

    Bug Fixes

    • update MDN and mime-db types (3e567a9)

    2.4.0 (2018-11-26)

    Features

    2.3.1 (2018-04-11)

    Bug Fixes

    2.3.0 (2018-04-11)

    ... (truncated)
    Commits
    • 6479a84 chore(release): 2.4.4
    • ed52b27 (chore) npm audit
    • 2f595e4 chore(release): 2.4.3
    • 3f33610 Merge branch 'marlon360-master'
    • f348aba patch: build .js instead of .json for types. fixes #208.
    • 30ba26d chore(release): 2.4.2
    • 4d59d76 Merge pull request #225 from vladgolubev/patch-1
    • 2e00b5c fix: don't use arrow function introduced in 2.4.1
    • bd2795a chore: Update platforms tested in travis
    • e3f7a2b chore(release): 2.4.1
    • Additional commits viewable in compare view

    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 0
  • Bump marked from 0.3.3 to 0.8.0

    Bump marked from 0.3.3 to 0.8.0

    Bumps marked from 0.3.3 to 0.8.0.

    Release notes

    Sourced from marked's releases.

    0.8.0

    Breaking changes

    Fixes

    • Fix relative urls in baseUrl option #1526
    • Loose task list #1535
    • Fix image parentheses #1557
    • remove module field & update devDependencies #1581

    Docs

    • Update examples with es6+ #1521
    • Fix link to USING_PRO.md page #1552
    • Fix typo in USING_ADVANCED.md #1558
    • Node worker threads are stable #1555

    Dev Dependencies

    • Update deps #1516
    • Update eslint #1542
    • Update htmldiffer async matcher #1543

    0.7.0

    Security

    • Sanitize paragraph and text tokens #1504
    • Fix ReDOS for links with backticks (issue #1493) #1515

    Breaking Changes

    • Deprecate sanitize and sanitizer options #1504
    • Move fences to CommonMark #1511
    • Move tables to GFM #1511
    • Remove tables option #1511
    • Single backtick in link text needs to be escaped #1515

    Fixes

    Tests

    • Run tests with correct options #1511

    0.6.3

    Fixes

    • Fix nested blockquotes #1464
    • Fix <em> issue with mixed content #1451
    ... (truncated)
    Commits
    Maintainer changes

    This version was pushed to npm by tonybrix, a new releaser for marked since your current version.


    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 0
  • Bump uglify-js from 1.3.3 to 3.8.0

    Bump uglify-js from 1.3.3 to 3.8.0

    Bumps uglify-js from 1.3.3 to 3.8.0.

    Release notes

    Sourced from uglify-js's releases.

    v3.8.0

     

    v3.7.7

     

    v3.7.6

     

    v3.7.5

     

    v3.7.4

     

    v3.7.3

     

    v3.7.2

     

    v3.7.1

     

    v3.7.0

     

    v3.6.9

     

    v3.6.8

     

    v3.6.7

     

    v3.6.6

     

    v3.6.5

     

    v3.6.4

     

    v3.6.3

     

    v3.6.2

     

    ... (truncated)
    Commits
    Maintainer changes

    This version was pushed to npm by alexlamsl, a new releaser for uglify-js since your current version.


    Dependabot compatibility score

    Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


    Dependabot commands and options

    You can trigger Dependabot actions by commenting on this PR:

    • @dependabot rebase will rebase this PR
    • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
    • @dependabot merge will merge this PR after your CI passes on it
    • @dependabot squash and merge will squash and merge this PR after your CI passes on it
    • @dependabot cancel merge will cancel a previously requested merge and block automerging
    • @dependabot reopen will reopen this PR if it is closed
    • @dependabot close will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
    • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
    • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    • @dependabot use these labels will set the current labels as the default for future PRs for this repo and language
    • @dependabot use these reviewers will set the current reviewers as the default for future PRs for this repo and language
    • @dependabot use these assignees will set the current assignees as the default for future PRs for this repo and language
    • @dependabot use this milestone will set the current milestone as the default for future PRs for this repo and language

    You can disable automated security fix PRs for this repo from the Security Alerts page.

    dependencies 
    opened by dependabot[bot] 0
  • Adding placeholder text to the editor?

    Adding placeholder text to the editor?

    Going by the documentation and a quick search through the code it doesn't seem to be possible to add a placeholder text to the editor. Is the the feature really not present? How hard would it be to add it?

    By the way, the official http://epiceditor.com seems to be down.

    Thanks

    opened by lfzawacki 4
  • EpicEditor.prototype.unload fails in an iframe

    EpicEditor.prototype.unload fails in an iframe

    1. Create a page that uses EpicEditor. Make sure you call unload.
    2. Load that page in an iframe.

    You will get an error because of this line:

    editor = window.parent.document.getElementById(self._instanceId);

    This seems like a bug. Is there any reason to be calling window.parent here? Even in the first commit, this line was there, yet there were no other calls to window.parent. Everything works fine with just window.

    opened by tansongyang 0
Releases(0.2.2)
  • 0.2.2(Aug 14, 2013)

    New Features

    • Added autogrow support, so the editor will get taller as you type (ticket #132) thanks @Gankro
    • You can now set the button bar to "show" (always on), "hide" (never show), and "auto" (default) (ticket #251) thanks @Gankro
    • exportFile now supports a json export type (ticket #237) thanks @Gankro

    Updates and Major Bug Fixes

    Source code(tar.gz)
    Source code(zip)
  • 0.2.1.1(Jul 7, 2013)

  • 0.2.1(Jul 7, 2013)

    New Features

    • There is now an autosave event so you can tell the difference between manual save()s and autosaves. It'll also only fire when the content has been changed since the last save. (ticket #233) thanks @Gankro
    • There's now a focus() method which you can use to easily focus on the editor. It handles cross browser differences as well as figuring out what to focus on (editor or previewer). (ticket #223)
    • If you wanted to hide / disable the fullscreen and/or preview toggle buttons, now you can (ticket #214) thanks @dobozysaurus
    • There is now a reflow event you can hook into to know when reflow() has been called. (ticket #210)
    • You can now customize the text of the tooltips that show up when hovering the icons like Preview, Edit, and Fullscreen. Useful for i18n (ticket #156) thanks @cmtt
    • The default Markdown parser, Marked, was upgraded giving you support for GitHub flavored tables, strikethroughs and breaks. (ticket #197)
    • Using the textarea option, you can now sync a textarea with the content of the editor. Useful for when you don't want to setup an AJAX form but want to use EE as a textarea replacement. (ticket #107) (ticket #140) (ticket #212)
    • Firefox now uses native fullscreen, and EpicEditor also support W3C fullscreen standard (ticket #22) thanks @nkovacs

    Updates and Major Bug Fixes

    • Fix for Firefox returning &nbsp;s in exported file contents in exportFile and getFiles. (ticket #204) thanks to @lamplightdev for the quick patch and @Gankro for the refactoring of getFiles and exportFile.
    • Theme paths with http or https now will ignore the basePath option so you link some themes to external CSS. (ticket #196)
    • Fixed preview content not being updated when autoSave is false. (ticket #195)
    • Fixed scrolling the whole web page when you clicked a hash link inside of EpicEditor in IE9 (ticket #165)
    • Cursor position is no longer lost when switching from preview to edit or vice versa in Firefox (ticket #139)
    • Fixed bug where, after doing the preview toggle keyboard shortcut in IE9, you'd lose focus on the editor (ticket #134)
    • Fixed IE9 fullscreen shortcut is triggering menu by changing the shortcut on windows to ctrl+alt+f (ticket #82)

    API Changes

    The only API change for this release is that the save event will no longer fire for autosaves. If you want to catch both you will need to listen for the autosave event as well as the save event now. Also note that save will fire for implicit manual saves such as open() or importFile().

    Behind-the-scenes Updates

    • Added .editorconfig file to make it easy for developers to follow code style. (ticket #220) thanks @hongymagic
    • Added jake watch which will automatically rebuild all the editor files if you modify src/* or if you edit the README.md, it'll run jake docs for you. (326539a00224cdb0c3ef0fb5005f017abe29fe82)
    Source code(tar.gz)
    Source code(zip)
  • 0.2.0(Jul 7, 2013)

    New Features

    • Complete Fullscreen API set. enterFullscreen() and exitFullscreen() methods and fullscreenenter and fullscreenexit events have been added (ticket #156) (ticket #146)
    • Added boolean useNativeFullscreen option (ticket #146)
    • Added is() method to get the state of the editor (ticket #88)
    • Added reflow(['width'|'height']) method to automatically refit the editor in the parent (ticket #160)

    Updates and Major Bug Fixes

    • Fixed character encoding bug causing strange characters over the wire in Chrome (ticket #184)
    • Fixed Firefox losing space characters (ticket #178)
    • Removed alt+o key command because it was causing problems on non-western keyboards and replaced with a toggling alt+p. (ticket #144) thanks @jeffhill
    • Preview state is now retained when loading after unloading (ticket #161)
    Source code(tar.gz)
    Source code(zip)
  • 0.1.1.1(Jul 7, 2013)

  • 0.1.1(Jul 7, 2013)

    New Features

    • getFiles() has been added allowing you to easily get all or one files in localStorage (ticket #85)
    • A boolean clientSideStorage option has been added. Setting it to false will make EpicEditor not save to localStorage (ticket #141)
    • EpicEditor now supports fluid width containers and will resize horizontally on the fly. (ticket #129)

    Updates and Major Bug Fixes

    • Fix for EpicEditor not loading in IE9 when run on the local file system (ticket #137)
    • Fix for exportFile() and file.content to export spaces as spaces and not &nbsp; characters (ticket #136)
    • Fix for update event firing before content was actually updated thus showing old content (ticket #135)
    • Fix for key shortcuts sometimes getting stuck and always firing and preventing you from typing certain characters (ticket #133) (ticket #127)
    • Fix for when clicking on the preview/edit buttons key shortcuts wouldn't work anymore until the editor was clicked again (ticket #125)
    • Fix for not being able to do fullscreen, close, fullscreen back to back in Firefox (ticket #121)
    • Fix for default file name becoming a stringified element object if there was no ID on the selected element (ticket #102)
    • Fixed anchor links opening in new tabs (ticket #96)
    • Fix for 2 <link> tags being loaded into the previewer iframe on load (ticket #78)
    • Safari now uses faux fullscreen (ticket #6)
    Source code(tar.gz)
    Source code(zip)
  • 0.1.0(Jul 7, 2013)

    API Changes

    • import is now importFile and has more functionality.
    • exportHTML is now exportFile and has more functionality.
    • get is now getElement with more selectors and slightly modified existing ones.
    • options is removed and moved as an object into the EpicEdior contructor.
    • Added version to EpicEditor object to return the current EE version (i.e. EpicEditor.version)

    New Features

    • Fullscreen in unsupported browsers (Firefox and IE9).
    • Save via ctrl/cmd+s.
    • Full CRUD event support (create, read, update, and remove events to hook into with on()).
    • Use any parser with the new parser option. Just return HTML from a function.

    Updates and Major Bug Fixes

    • EpicEditor gets a logo from @sebnitu.
    • Complete IE9 support.
    • Theming has been completely redone. Instead of everything living in one document and conflicting with each other it will live in these multiple documents: base, editor, and preview.
    • New and much better Markdown parser Marked by @chjj. (EpicEditor's own README actually works in EpicEditor finally).
    • New icons with preview/edit specific icons (thanks @sebnitu).
    • Better docs (thanks again @sebnitu).
    • Updated GitHub theme to match their current theme.
    • Better interval cleanup on unload()ing

    Behind-the-scenes Updates

    • Full test coverage on every EpicEditor API method using foounit by @foobarfighter
    • Docs are generated from the README
    • Real builds from the /src directory
    • Marked (Markdown parser) has been pulled out from the source and is a git submodule
    Source code(tar.gz)
    Source code(zip)
  • 0.0.3(Jul 7, 2013)

Owner
Oscar Godson
Partner at Bickford+Godson, and previously of Acorns (Director of Engineering), Vault (CTO), Piggybank (founder), Simple, Microsoft, and Yammer.
Oscar Godson
enjoy live editing (+markdown)

Pen Editor LIVE DEMO: http://sofish.github.io/pen Markdown is supported Build status: 0. source code You can clone the source code from github, or usi

小鱼 4.8k Dec 24, 2022
:notebook: Our cool, secure, and offline-first Markdown editor.

Monod Hi! I'm Monod, the Markdown Editor! Monod is a (relatively) secure and offline-first Markdown editor we have built at TailorDev in order to lear

TailorDev 877 Dec 4, 2022
A markdown editor. http://lab.lepture.com/editor/

Editor A markdown editor you really want. Sponsors Editor is sponsored by Typlog. Overview Editor is not a WYSIWYG editor, it is a plain text markdown

Hsiaoming Yang 2.8k Dec 19, 2022
The world's #1 JavaScript library for rich text editing. Available for React, Vue and Angular

TinyMCE TinyMCE is the world's most advanced open source core rich text editor. Trusted by millions of developers, and used by some of the world's lar

Tiny 12.4k Jan 4, 2023
Override the rich text editor in Strapi admin with ToastUI Editor.

strapi-plugin-wysiwyg-tui-editor ⚠️ This is a strapi v4 plugin which does not support any earlier version! A Strapi plugin to replace the default rich

Zhuo Chen 12 Dec 23, 2022
A chrome extension which helps change ace editor to monaco editor in web pages, supporting all features including autocompletes.

Monaco-It Monaco-It is a chrome extension turning Ace Editor into Monaco Editor, supporting all features including autocompletes. 一些中文说明 Supported Lan

null 3 May 17, 2022
Add to your GitHub readme a badge that shows your Discord username and presence (online/idle/do not disturb/offline)!

Discord Profile Markdown badge Add to your GitHub readme a badge that shows your Discord username and presence! Set up Join the Discord server (requir

Monty 82 Dec 30, 2022
Like codepen and jsbin but works offline.

Like codepen and jsbin but works offline.

EGOIST 1.1k Jan 2, 2023
A markdown editor using Electron, ReactJS, Vite, CodeMirror, and Remark

updated: Saturday, 5th February 2022 A modern looking application built with Electron-Vite-React ?? ✨ Markdown Editor Introduction This application s

Kryptonite 5 Sep 7, 2022
🍞📝 Markdown WYSIWYG Editor. GFM Standard + Chart & UML Extensible.

TOAST UI Editor v3 major update planning ?? ?? ?? TOAST UI Editor is planning a v3 major update for 2021. You can see our detail RoadMap here! GFM Mar

NHN 15.5k Jan 3, 2023
Easily convert markdown files to PDF

ezPDF What's this? This is a simple markdown to pdf parser that supports custom CSS stylesheets. In the future, ezPDF will allow you to preview files

Matheus 12 Oct 11, 2022
In-browser Markdown editor

StackEdit Full-featured, open-source Markdown editor based on PageDown, the Markdown library used by Stack Overflow and the other Stack Exchange sites

Benoit Schweblin 19.9k Jan 9, 2023
:herb: NodeJS PHP Parser - extract AST or tokens (PHP5 and PHP7)

php-parser This javascript library parses PHP code and convert it to AST. Installation This library is distributed with npm : npm install php-parser -

glayzzle 476 Jan 7, 2023
Medium.com WYSIWYG editor clone. Uses contenteditable API to implement a rich text solution.

If you would be interested in helping to maintain one of the most successful WYSIWYG text editors on github, let us know! (See issue #1503) MediumEdit

yabwe 15.7k Jan 4, 2023
A lightweight and amazing WYSIWYG JavaScript editor - 20kB only (8kB gzip)

Supporting Trumbowyg Trumbowyg is an MIT-licensed open source project and completely free to use. However, the amount of effort needed to maintain and

Alexandre Demode 3.8k Jan 7, 2023
The next generation Javascript WYSIWYG HTML Editor.

Froala Editor V3 Froala WYSIWYG HTML Editor is one of the most powerful JavaScript rich text editors ever. Slim - only add the plugins that you need (

Froala 5k Jan 1, 2023
A powerful WYSIWYG rich text web editor by pure javascript

KothingEditor A powerful WYSIWYG rich text web editor by pure javascript Demo : kothing.github.io/editor The KothingEditor is a lightweight, flexible,

Kothing 34 Dec 25, 2022
Pure javascript based WYSIWYG html editor, with no dependencies.

SunEditor Pure javscript based WYSIWYG web editor, with no dependencies Demo : suneditor.com The Suneditor is a lightweight, flexible, customizable WY

Yi JiHong 1.1k Jan 2, 2023
Typewriter is a simple, FOSS, Web-based text editor that aims to provide a simple and intuitive environment for you to write in.

Typewriter Typewriter is a simple, FOSS, Web-based text editor that aims to provide a simple and intuitive environment for you to write in. Features S

Isla 2 May 24, 2022