From 73fbf49ba4cb3603b7f381ec9e3626b5efa0537d Mon Sep 17 00:00:00 2001 From: Baptiste Augrain Date: Tue, 21 Aug 2018 00:19:26 +0200 Subject: [PATCH 01/56] - show tags of note in alphabetical order - enable live count of notes --- browser/components/NoteItem.js | 16 ++++-- browser/components/TagListItem.js | 2 +- browser/main/Detail/MarkdownNoteDetail.js | 3 +- browser/main/Detail/SnippetNoteDetail.js | 1 + browser/main/Detail/TagSelect.js | 4 +- browser/main/NoteList/index.js | 1 + browser/main/SideNav/index.js | 18 +++++- browser/main/modals/PreferencesModal/UiTab.js | 55 +++++++++++++++---- locales/en.json | 4 +- locales/fr.json | 4 +- 10 files changed, 81 insertions(+), 27 deletions(-) diff --git a/browser/components/NoteItem.js b/browser/components/NoteItem.js index 5073dc73..b0731eca 100644 --- a/browser/components/NoteItem.js +++ b/browser/components/NoteItem.js @@ -24,16 +24,19 @@ const TagElement = ({ tagName }) => ( /** * @description Tag element list component. * @param {Array|null} tags + * @param {boolean} showTagsAlphabetically * @return {React.Component} */ -const TagElementList = tags => { +const TagElementList = (tags, showTagsAlphabetically) => { if (!isArray(tags)) { return [] } - const tagElements = tags.map(tag => TagElement({ tagName: tag })) - - return tagElements + if (showTagsAlphabetically) { + return _.sortBy(tags).map(tag => TagElement({ tagName: tag })) + } else { + return tags.map(tag => TagElement({ tagName: tag })) + } } /** @@ -55,7 +58,8 @@ const NoteItem = ({ pathname, storageName, folderName, - viewType + viewType, + showTagsAlphabetically }) => (
{note.tags.length > 0 - ? TagElementList(note.tags) + ? TagElementList(note.tags, showTagsAlphabetically) : handleClickTagListItem(name)}> {`# ${name}`} - {count} + {count !== 0 ? count : ''}
diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index 82073162..705736e9 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -312,7 +312,7 @@ class MarkdownNoteDetail extends React.Component { } render () { - const { data, location } = this.props + const { data, location, config } = this.props const { note, editorType } = this.state const storageKey = note.storage const folderKey = note.folder @@ -363,6 +363,7 @@ class MarkdownNoteDetail extends React.Component { diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 652d1f53..3633ec85 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -744,6 +744,7 @@ class SnippetNoteDetail extends React.Component { this.handleChange(e)} />
diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js index e251dd42..e51d5673 100644 --- a/browser/main/Detail/TagSelect.js +++ b/browser/main/Detail/TagSelect.js @@ -119,10 +119,10 @@ class TagSelect extends React.Component { } render () { - const { value, className } = this.props + const { value, className, showTagsAlphabetically } = this.props const tagList = _.isArray(value) - ? value.map((tag) => { + ? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => { return ( ) } diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index c4fa417b..d1292f68 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -19,6 +19,10 @@ import {SortableContainer} from 'react-sortable-hoc' import i18n from 'browser/lib/i18n' import context from 'browser/lib/context' +function findOne(haystack, arr) { + return arr.some(v => haystack.indexOf(v) >= 0) +} + class SideNav extends React.Component { // TODO: should not use electron stuff v0.7 @@ -144,12 +148,20 @@ class SideNav extends React.Component { tagListComponent () { const { data, location, config } = this.props - const relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap) + const activeTags = this.getActiveTags(location.pathname) + const relatedTags = this.getRelatedTags(activeTags, data.noteMap) let tagList = _.sortBy(data.tagNoteMap.map( (tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) }) - ), ['name']).filter( + ).filter( tag => tag.size > 0 - ) + ), ['name']) + if (config.ui.enableLiveNoteCounts && activeTags.length !== 0) { + const notesTags = data.noteMap.map(note => note.tags) + tagList = tagList.map(tag => { + tag.size = notesTags.filter(tags => tags.includes(tag.name) && findOne(tags, activeTags)).length + return tag + }) + } if (config.sortTagsBy === 'COUNTER') { tagList = _.sortBy(tagList, item => (0 - item.size)) } diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index aa3568e7..40269190 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -70,6 +70,8 @@ class UiTab extends React.Component { showCopyNotification: this.refs.showCopyNotification.checked, confirmDeletion: this.refs.confirmDeletion.checked, showOnlyRelatedTags: this.refs.showOnlyRelatedTags.checked, + showTagsAlphabetically: this.refs.showTagsAlphabetically.checked, + enableLiveNoteCounts: this.refs.enableLiveNoteCounts.checked, disableDirectWrite: this.refs.uiD2w != null ? this.refs.uiD2w.checked : false @@ -172,7 +174,9 @@ class UiTab extends React.Component {
{i18n.__('Interface')}
- {i18n.__('Interface Theme')} +
+ {i18n.__('Interface Theme')} +
this.handleUIChange(e)} @@ -221,16 +227,6 @@ class UiTab extends React.Component { {i18n.__('Show a confirmation dialog when deleting notes')}
-
- -
{ global.process.platform === 'win32' ?
@@ -246,6 +242,41 @@ class UiTab extends React.Component {
: null } +
Tags
+ +
+ +
+ +
+ +
+ +
+ +
+
Editor
diff --git a/locales/en.json b/locales/en.json index a9767492..d01a6175 100644 --- a/locales/en.json +++ b/locales/en.json @@ -175,5 +175,7 @@ "Allow styles": "Allow styles", "Allow dangerous html tags": "Allow dangerous html tags", "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.", - "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠" + "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠", + "Show tags of a note in alphabetical order": "Show tags of a note in alphabetical order", + "Enable live count of notes": "Enable live count of notes" } diff --git a/locales/fr.json b/locales/fr.json index 8b880aa6..33a7c503 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -152,5 +152,7 @@ "Allow styles": "Accepter les styles", "Allow dangerous html tags": "Accepter les tags html dangereux", "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convertir des flèches textuelles en jolis signes. ⚠ Cela va interferérer avec les éventuels commentaires HTML dans votre Markdown.", - "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ Vous avez collé un lien qui référence une pièce-jointe qui n'a pas pu être récupéré dans le dossier de stockage de la note. Coller des liens qui font référence à des pièces-jointes ne fonctionne que si la source et la destination et la même. Veuillez plutôt utiliser du Drag & Drop ! ⚠" + "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ Vous avez collé un lien qui référence une pièce-jointe qui n'a pas pu être récupéré dans le dossier de stockage de la note. Coller des liens qui font référence à des pièces-jointes ne fonctionne que si la source et la destination et la même. Veuillez plutôt utiliser du Drag & Drop ! ⚠", + "Show tags of a note in alphabetical order": "Afficher les tags d'une note par ordre alphabétique", + "Enable live count of notes": "Activer le comptage live des notes" } From 7cde30d3522a4c23d6995ccb68393a12ab6c85e1 Mon Sep 17 00:00:00 2001 From: Baptiste Augrain Date: Tue, 21 Aug 2018 00:24:03 +0200 Subject: [PATCH 02/56] fix lint errors --- browser/main/SideNav/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index d1292f68..0dd34989 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -19,8 +19,8 @@ import {SortableContainer} from 'react-sortable-hoc' import i18n from 'browser/lib/i18n' import context from 'browser/lib/context' -function findOne(haystack, arr) { - return arr.some(v => haystack.indexOf(v) >= 0) +function findOne (haystack, arr) { + return arr.some(v => haystack.indexOf(v) >= 0) } class SideNav extends React.Component { From a59100176184b9700da1cb1b838106a6baf6736c Mon Sep 17 00:00:00 2001 From: Baptiste Augrain Date: Sat, 25 Aug 2018 20:18:59 +0200 Subject: [PATCH 03/56] add parameter to specify the height of the diagram, fixing #2335 --- browser/components/render/MermaidRender.js | 1 + browser/lib/markdown.js | 2 +- browser/styles/index.styl | 6 +++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/browser/components/render/MermaidRender.js b/browser/components/render/MermaidRender.js index 12dce327..bac52b2c 100644 --- a/browser/components/render/MermaidRender.js +++ b/browser/components/render/MermaidRender.js @@ -28,6 +28,7 @@ function render (element, content, theme) { }) mermaidAPI.render(getId(), content, (svgGraph) => { element.innerHTML = svgGraph + element.style.height = element.attributes.getNamedItem('data-height').value + 'vh' }) } catch (e) { console.error(e) diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 49fd2f86..d4bc72e4 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -44,7 +44,7 @@ class Markdown { return `
${str}
` } if (langType === 'mermaid') { - return `
${str}
` + return `
${str}
` } return '
' +
           '' + fileName + '' +
diff --git a/browser/styles/index.styl b/browser/styles/index.styl
index 7d32e77a..8f917fcf 100644
--- a/browser/styles/index.styl
+++ b/browser/styles/index.styl
@@ -383,4 +383,8 @@ modalmonokai()
   width 100%
   background-color $ui-monokai-backgroundColor
   overflow hidden
-  border-radius $modal-border-radius
\ No newline at end of file
+  border-radius $modal-border-radius
+
+pre.mermaid svg {
+  max-width 100% !important
+}
\ No newline at end of file

From 3bdc88cecb6ed117312e2860e3c84afc5b803d69 Mon Sep 17 00:00:00 2001
From: Baptiste Augrain 
Date: Sat, 25 Aug 2018 23:14:05 +0200
Subject: [PATCH 04/56] fixing sanitization of inline html like () #1992

---
 browser/lib/markdown-it-sanitize-html.js | 86 +++++++++++++++++++++++-
 browser/lib/markdown.js                  |  6 +-
 2 files changed, 90 insertions(+), 2 deletions(-)

diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js
index 05e5e7be..ea27bfa0 100644
--- a/browser/lib/markdown-it-sanitize-html.js
+++ b/browser/lib/markdown-it-sanitize-html.js
@@ -2,6 +2,7 @@
 
 import sanitizeHtml from 'sanitize-html'
 import { escapeHtmlCharacters } from './utils'
+import url from 'url'
 
 module.exports = function sanitizePlugin (md, options) {
   options = options || {}
@@ -25,7 +26,7 @@ module.exports = function sanitizePlugin (md, options) {
         const inlineTokens = state.tokens[tokenIdx].children
         for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) {
           if (inlineTokens[childIdx].type === 'html_inline') {
-            inlineTokens[childIdx].content = sanitizeHtml(
+            inlineTokens[childIdx].content = sanitizeInline(
               inlineTokens[childIdx].content,
               options
             )
@@ -35,3 +36,86 @@ module.exports = function sanitizePlugin (md, options) {
     }
   })
 }
+
+const tag_regex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:="(?:[^\"]+)\")?)*)\s*>|<\/([A-Z][A-Z0-9]*)\s*>/i
+const attributes_regex = /([A-Z][A-Z0-9]*)(="[^\"]+\")?/ig
+
+function sanitizeInline(html, options) {
+  let match = tag_regex.exec(html)
+  if (!match) {
+    return ''
+  }
+  
+  const { allowedTags, allowedAttributes, allowedIframeHostnames, selfClosing, allowedSchemesAppliedToAttributes } = options
+  
+  if (match[1] !== null) {
+    // opening tag
+    const tag = match[1].toLowerCase()
+    if (allowedTags.indexOf(tag) === -1) {
+      return ''
+    }
+    
+    const attributes = match[2]
+    
+    let attrs = ''
+    let name
+    let value
+    
+    while ((match = attributes_regex.exec(attributes))) {
+      name = match[1].toLowerCase()
+      value = match[2]
+      
+      if (allowedAttributes['*'].indexOf(name) !== -1 || (allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)) {
+        if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) {
+          if (naughtyHRef(value) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value))) {
+            continue
+          }
+        }
+        
+        attrs += ` ${name}${value}`
+      }
+    }
+    
+    if (selfClosing.indexOf(tag)) {
+      return '<' + tag + attrs + ' />'
+    } else {
+      return '<' + tag + attrs + '>'
+    }
+  } else {
+    // closing tag
+    if (allowedTags.indexOf(match[3].toLowerCase()) !== -1) {
+      return html
+    } else {
+      return ''
+    }
+  }
+}
+
+function naughtyHRef(name, href, options) {
+  href = href.replace(/[\x00-\x20]+/g, '')
+  href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
+  
+  const matches = href.match(/^([a-zA-Z]+)\:/)
+  if (!matches) {
+    if (href.match(/^[\/\\]{2}/)) {
+      return !options.allowProtocolRelative
+    }
+
+    // No scheme
+    return false
+  }
+
+  const scheme = matches[1].toLowerCase()
+
+  return options.allowedSchemes.indexOf(scheme) === -1
+}
+
+function naughtyIFrame(src) {
+  try {
+    const parsed = url.parse(src, false, true)
+    
+    return allowedIframeHostnames.index(parsed.hostname) === -1
+  } catch (e) {
+    return true
+  }
+}
\ No newline at end of file
diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js
index 49fd2f86..90b1ccea 100644
--- a/browser/lib/markdown.js
+++ b/browser/lib/markdown.js
@@ -105,7 +105,11 @@ class Markdown {
           'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
           'input': ['type', 'id', 'checked']
         },
-        allowedIframeHostnames: ['www.youtube.com']
+        allowedIframeHostnames: ['www.youtube.com'],
+        selfClosing: [ 'img', 'br', 'hr', 'input' ],
+        allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
+        allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
+        allowProtocolRelative: true
       })
     }
 

From fabc975b2025c12402312862c26abfb12bcc00b4 Mon Sep 17 00:00:00 2001
From: Baptiste Augrain 
Date: Sat, 25 Aug 2018 23:36:43 +0200
Subject: [PATCH 05/56] - fix lint errors - correctly parse self-closed tag -
 fix naughty functions

---
 browser/lib/markdown-it-sanitize-html.js | 46 ++++++++++++------------
 1 file changed, 23 insertions(+), 23 deletions(-)

diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js
index ea27bfa0..9bdd3034 100644
--- a/browser/lib/markdown-it-sanitize-html.js
+++ b/browser/lib/markdown-it-sanitize-html.js
@@ -37,45 +37,45 @@ module.exports = function sanitizePlugin (md, options) {
   })
 }
 
-const tag_regex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:="(?:[^\"]+)\")?)*)\s*>|<\/([A-Z][A-Z0-9]*)\s*>/i
-const attributes_regex = /([A-Z][A-Z0-9]*)(="[^\"]+\")?/ig
+const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:="(?:[^\"]+)\")?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i
+const attributesRegex = /([A-Z][A-Z0-9]*)(="[^\"]+\")?/ig
 
-function sanitizeInline(html, options) {
-  let match = tag_regex.exec(html)
+function sanitizeInline (html, options) {
+  let match = tagRegex.exec(html)
   if (!match) {
     return ''
   }
-  
-  const { allowedTags, allowedAttributes, allowedIframeHostnames, selfClosing, allowedSchemesAppliedToAttributes } = options
-  
-  if (match[1] !== null) {
+
+  const { allowedTags, allowedAttributes, selfClosing, allowedSchemesAppliedToAttributes } = options
+
+  if (match[1] !== undefined) {
     // opening tag
     const tag = match[1].toLowerCase()
     if (allowedTags.indexOf(tag) === -1) {
       return ''
     }
-    
+
     const attributes = match[2]
-    
+
     let attrs = ''
     let name
     let value
-    
-    while ((match = attributes_regex.exec(attributes))) {
+
+    while ((match = attributesRegex.exec(attributes))) {
       name = match[1].toLowerCase()
       value = match[2]
-      
+
       if (allowedAttributes['*'].indexOf(name) !== -1 || (allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)) {
         if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) {
-          if (naughtyHRef(value) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value))) {
+          if (naughtyHRef(value, options) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value, options))) {
             continue
           }
         }
-        
+
         attrs += ` ${name}${value}`
       }
     }
-    
+
     if (selfClosing.indexOf(tag)) {
       return '<' + tag + attrs + ' />'
     } else {
@@ -91,10 +91,10 @@ function sanitizeInline(html, options) {
   }
 }
 
-function naughtyHRef(name, href, options) {
-  href = href.replace(/[\x00-\x20]+/g, '')
+function naughtyHRef (href, options) {
+  // href = href.replace(/[\x00-\x20]+/g, '')
   href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
-  
+
   const matches = href.match(/^([a-zA-Z]+)\:/)
   if (!matches) {
     if (href.match(/^[\/\\]{2}/)) {
@@ -110,12 +110,12 @@ function naughtyHRef(name, href, options) {
   return options.allowedSchemes.indexOf(scheme) === -1
 }
 
-function naughtyIFrame(src) {
+function naughtyIFrame (src, options) {
   try {
     const parsed = url.parse(src, false, true)
-    
-    return allowedIframeHostnames.index(parsed.hostname) === -1
+
+    return options.allowedIframeHostnames.index(parsed.hostname) === -1
   } catch (e) {
     return true
   }
-}
\ No newline at end of file
+}

From 2a838ebb0b46bad08f54f46256396fac1c52c02a Mon Sep 17 00:00:00 2001
From: Baptiste Augrain 
Date: Sun, 26 Aug 2018 00:14:29 +0200
Subject: [PATCH 06/56] fixing single quoted attributes

---
 browser/lib/markdown-it-sanitize-html.js | 13 ++++++++-----
 1 file changed, 8 insertions(+), 5 deletions(-)

diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js
index 9bdd3034..ce6c5e29 100644
--- a/browser/lib/markdown-it-sanitize-html.js
+++ b/browser/lib/markdown-it-sanitize-html.js
@@ -37,8 +37,8 @@ module.exports = function sanitizePlugin (md, options) {
   })
 }
 
-const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:="(?:[^\"]+)\")?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i
-const attributesRegex = /([A-Z][A-Z0-9]*)(="[^\"]+\")?/ig
+const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:=("|')(?:[^\3]+?)\3)?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i
+const attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/ig
 
 function sanitizeInline (html, options) {
   let match = tagRegex.exec(html)
@@ -63,7 +63,7 @@ function sanitizeInline (html, options) {
 
     while ((match = attributesRegex.exec(attributes))) {
       name = match[1].toLowerCase()
-      value = match[2]
+      value = match[3]
 
       if (allowedAttributes['*'].indexOf(name) !== -1 || (allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)) {
         if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) {
@@ -72,7 +72,10 @@ function sanitizeInline (html, options) {
           }
         }
 
-        attrs += ` ${name}${value}`
+        attrs += ` ${name}`
+        if (match[2]) {
+          attrs += `="${value}"`
+        }
       }
     }
 
@@ -83,7 +86,7 @@ function sanitizeInline (html, options) {
     }
   } else {
     // closing tag
-    if (allowedTags.indexOf(match[3].toLowerCase()) !== -1) {
+    if (allowedTags.indexOf(match[4].toLowerCase()) !== -1) {
       return html
     } else {
       return ''

From 094e4c5da82b869a383c8a5e98847ea84edf9698 Mon Sep 17 00:00:00 2001
From: Baptiste Augrain 
Date: Mon, 27 Aug 2018 02:41:56 +0200
Subject: [PATCH 07/56] add support to abbreviations, subscript text,
 superscript text and definition lists

---
 browser/components/markdown.styl   |  55 ++++++-
 browser/lib/markdown-it-deflist.js | 221 +++++++++++++++++++++++++++++
 browser/lib/markdown.js            |   4 +
 package.json                       |   3 +
 yarn.lock                          |  12 ++
 5 files changed, 294 insertions(+), 1 deletion(-)
 create mode 100644 browser/lib/markdown-it-deflist.js

diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl
index 03503231..4f326e08 100644
--- a/browser/components/markdown.styl
+++ b/browser/components/markdown.styl
@@ -371,6 +371,35 @@ for name, val in admonition_types
         color: val[color]
         content: val[icon]
 
+dl
+  margin 2em 0
+  padding 0
+  display flex
+  width 100%
+  flex-wrap wrap
+  align-items flex-start
+  border-bottom 1px solid borderColor
+  background-color tableHeadBgColor
+
+dt
+  border-top 1px solid borderColor
+  font-weight bold
+  text-align right
+  overflow hidden
+  flex-basis 18%
+  padding 1%
+
+dd
+  border-top 1px solid borderColor
+  flex-basis 78%
+  max-width 78%
+  padding 1%
+  min-height 1.55em
+  background-color $ui-noteDetail-backgroundColor
+
+dd + dd
+  margin-left 20%
+
 themeDarkBackground = darken(#21252B, 10%)
 themeDarkText = #f9f9f9
 themeDarkBorder = lighten(themeDarkBackground, 20%)
@@ -421,6 +450,14 @@ body[data-theme="dark"]
   kbd
     background-color themeDarkBorder
     color themeDarkText
+  dl
+    border-color themeDarkBorder
+    background-color themeDarkTableHead
+  dt
+    border-color themeDarkBorder
+  dd
+    border-color themeDarkBorder
+    background-color themeDarkPreview
 
 themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
 themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
@@ -448,6 +485,14 @@ body[data-theme="solarized-dark"]
         border-color themeSolarizedDarkTableBorder
         &:last-child
           border-right solid 1px themeSolarizedDarkTableBorder
+  dl
+    border-color themeDarkBorder
+    background-color themeSolarizedDarkTableHead
+  dt
+    border-color themeDarkBorder
+  dd
+    border-color themeDarkBorder
+    background-color $ui-solarized-dark-noteDetail-backgroundColor
 
 themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
 themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
@@ -476,4 +521,12 @@ body[data-theme="monokai"]
         &:last-child
           border-right solid 1px themeMonokaiTableBorder
   kbd
-    background-color themeDarkBackground
\ No newline at end of file
+    background-color themeDarkBackground
+  dl
+    border-color themeDarkBorder
+    background-color themeMonokaiTableHead
+  dt
+    border-color themeDarkBorder
+  dd
+    border-color themeDarkBorder
+    background-color $ui-monokai-noteDetail-backgroundColor
\ No newline at end of file
diff --git a/browser/lib/markdown-it-deflist.js b/browser/lib/markdown-it-deflist.js
new file mode 100644
index 00000000..5dd02267
--- /dev/null
+++ b/browser/lib/markdown-it-deflist.js
@@ -0,0 +1,221 @@
+'use strict'
+
+module.exports = function definitionListPlugin (md) {
+  var isSpace = md.utils.isSpace
+
+  // Search `[:~][\n ]`, returns next pos after marker on success
+  // or -1 on fail.
+  function skipMarker (state, line) {
+    let start = state.bMarks[line] + state.tShift[line]
+    const max = state.eMarks[line]
+
+    if (start >= max) { return -1 }
+
+    // Check bullet
+    const marker = state.src.charCodeAt(start++)
+    if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1 }
+
+    const pos = state.skipSpaces(start)
+
+    // require space after ":"
+    if (start === pos) { return -1 }
+
+    return start
+  }
+
+  function markTightParagraphs (state, idx) {
+    const level = state.level + 2
+
+    let i
+    let l
+    for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
+      if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
+        state.tokens[i + 2].hidden = true
+        state.tokens[i].hidden = true
+        i += 2
+      }
+    }
+  }
+
+  function deflist (state, startLine, endLine, silent) {
+    var ch,
+      contentStart,
+      ddLine,
+      dtLine,
+      itemLines,
+      listLines,
+      listTokIdx,
+      max,
+      nextLine,
+      offset,
+      oldDDIndent,
+      oldIndent,
+      oldParentType,
+      oldSCount,
+      oldTShift,
+      oldTight,
+      pos,
+      prevEmptyEnd,
+      tight,
+      token
+
+    if (silent) {
+      // quirk: validation mode validates a dd block only, not a whole deflist
+      if (state.ddIndent < 0) { return false }
+      return skipMarker(state, startLine) >= 0
+    }
+
+    nextLine = startLine + 1
+    if (nextLine >= endLine) { return false }
+
+    if (state.isEmpty(nextLine)) {
+      nextLine++
+      if (nextLine >= endLine) { return false }
+    }
+
+    if (state.sCount[nextLine] < state.blkIndent) { return false }
+    contentStart = skipMarker(state, nextLine)
+    if (contentStart < 0) { return false }
+
+    // Start list
+    listTokIdx = state.tokens.length
+    tight = true
+
+    token = state.push('dl_open', 'dl', 1)
+    token.map = listLines = [ startLine, 0 ]
+
+    //
+    // Iterate list items
+    //
+
+    dtLine = startLine
+    ddLine = nextLine
+
+    // One definition list can contain multiple DTs,
+    // and one DT can be followed by multiple DDs.
+    //
+    // Thus, there is two loops here, and label is
+    // needed to break out of the second one
+    //
+    /* eslint no-labels:0,block-scoped-var:0 */
+    OUTER:
+    for (;;) {
+      prevEmptyEnd = false
+
+      token = state.push('dt_open', 'dt', 1)
+      token.map = [ dtLine, dtLine ]
+
+      token = state.push('inline', '', 0)
+      token.map = [ dtLine, dtLine ]
+      token.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim()
+      token.children = []
+
+      token = state.push('dt_close', 'dt', -1)
+
+      for (;;) {
+        token = state.push('dd_open', 'dd', 1)
+        token.map = itemLines = [ nextLine, 0 ]
+
+        pos = contentStart
+        max = state.eMarks[ddLine]
+        offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine])
+
+        while (pos < max) {
+          ch = state.src.charCodeAt(pos)
+
+          if (isSpace(ch)) {
+            if (ch === 0x09) {
+              offset += 4 - offset % 4
+            } else {
+              offset++
+            }
+          } else {
+            break
+          }
+
+          pos++
+        }
+
+        contentStart = pos
+
+        oldTight = state.tight
+        oldDDIndent = state.ddIndent
+        oldIndent = state.blkIndent
+        oldTShift = state.tShift[ddLine]
+        oldSCount = state.sCount[ddLine]
+        oldParentType = state.parentType
+        state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2
+        state.tShift[ddLine] = contentStart - state.bMarks[ddLine]
+        state.sCount[ddLine] = offset
+        state.tight = true
+        state.parentType = 'deflist'
+
+        state.md.block.tokenize(state, ddLine, endLine, true)
+
+        // If any of list item is tight, mark list as tight
+        if (!state.tight || prevEmptyEnd) {
+          tight = false
+        }
+        // Item become loose if finish with empty line,
+        // but we should filter last element, because it means list finish
+        prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1)
+
+        state.tShift[ddLine] = oldTShift
+        state.sCount[ddLine] = oldSCount
+        state.tight = oldTight
+        state.parentType = oldParentType
+        state.blkIndent = oldIndent
+        state.ddIndent = oldDDIndent
+
+        token = state.push('dd_close', 'dd', -1)
+
+        itemLines[1] = nextLine = state.line
+
+        if (nextLine >= endLine) { break OUTER }
+
+        if (state.sCount[nextLine] < state.blkIndent) { break OUTER }
+        contentStart = skipMarker(state, nextLine)
+        if (contentStart < 0) { break }
+
+        ddLine = nextLine
+
+        // go to the next loop iteration:
+        // insert DD tag and repeat checking
+      }
+
+      if (nextLine >= endLine) { break }
+      dtLine = nextLine
+
+      if (state.isEmpty(dtLine)) { break }
+      if (state.sCount[dtLine] < state.blkIndent) { break }
+
+      ddLine = dtLine + 1
+      if (ddLine >= endLine) { break }
+      if (state.isEmpty(ddLine)) { ddLine++ }
+      if (ddLine >= endLine) { break }
+
+      if (state.sCount[ddLine] < state.blkIndent) { break }
+      contentStart = skipMarker(state, ddLine)
+      if (contentStart < 0) { break }
+
+      // go to the next loop iteration:
+      // insert DT and DD tags and repeat checking
+    }
+
+    // Finilize list
+    token = state.push('dl_close', 'dl', -1)
+
+    listLines[1] = nextLine
+
+    state.line = nextLine
+
+    // mark paragraphs tight if needed
+    if (tight) {
+      markTightParagraphs(state, listTokIdx)
+    }
+
+    return true
+  }
+
+  md.block.ruler.before('paragraph', 'deflist', deflist, { alt: [ 'paragraph', 'reference' ] })
+}
diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js
index 49fd2f86..a3e230af 100644
--- a/browser/lib/markdown.js
+++ b/browser/lib/markdown.js
@@ -149,6 +149,10 @@ class Markdown {
     })
     this.md.use(require('markdown-it-kbd'))
     this.md.use(require('markdown-it-admonition'))
+    this.md.use(require('markdown-it-abbr'))
+    this.md.use(require('markdown-it-sub'))
+    this.md.use(require('markdown-it-sup'))
+    this.md.use(require('./markdown-it-deflist'))
 
     const deflate = require('markdown-it-plantuml/lib/deflate')
     this.md.use(require('markdown-it-plantuml'), '', {
diff --git a/package.json b/package.json
index e9949adf..ee814f3b 100644
--- a/package.json
+++ b/package.json
@@ -71,6 +71,7 @@
     "lodash": "^4.11.1",
     "lodash-move": "^1.1.1",
     "markdown-it": "^6.0.1",
+    "markdown-it-abbr": "^1.0.4",
     "markdown-it-admonition": "^1.0.4",
     "markdown-it-emoji": "^1.1.1",
     "markdown-it-footnote": "^3.0.0",
@@ -80,6 +81,8 @@
     "markdown-it-named-headers": "^0.0.4",
     "markdown-it-plantuml": "^1.1.0",
     "markdown-it-smartarrows": "^1.0.1",
+    "markdown-it-sub": "^1.0.0",
+    "markdown-it-sup": "^1.0.0",
     "mdurl": "^1.0.1",
     "mermaid": "^8.0.0-rc.8",
     "moment": "^2.10.3",
diff --git a/yarn.lock b/yarn.lock
index 4ecfa51b..d32f1520 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5723,6 +5723,10 @@ map-visit@^1.0.0:
   dependencies:
     object-visit "^1.0.0"
 
+markdown-it-abbr@^1.0.4:
+  version "1.0.4"
+  resolved "https://registry.yarnpkg.com/markdown-it-abbr/-/markdown-it-abbr-1.0.4.tgz#d66b5364521cbb3dd8aa59dadfba2fb6865c8fd8"
+
 markdown-it-admonition@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/markdown-it-admonition/-/markdown-it-admonition-1.0.4.tgz#d7bbc7eb1fe6168fc8cc304de7a9d8c993acb2f5"
@@ -5763,6 +5767,14 @@ markdown-it-smartarrows@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/markdown-it-smartarrows/-/markdown-it-smartarrows-1.0.1.tgz#b570e9c0ff9812e0db6ace19afa5ba12b64bb9a7"
 
+markdown-it-sub@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/markdown-it-sub/-/markdown-it-sub-1.0.0.tgz#375fd6026eae7ddcb012497f6411195ea1e3afe8"
+
+markdown-it-sup@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/markdown-it-sup/-/markdown-it-sup-1.0.0.tgz#cb9c9ff91a5255ac08f3fd3d63286e15df0a1fc3"
+
 markdown-it@^5.0.3:
   version "5.1.0"
   resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-5.1.0.tgz#25286b8465bac496f3f1b77eed544643e9bd718d"

From 646151e02081850df4b916a619540b9e3e6c735f Mon Sep 17 00:00:00 2001
From: Baptiste Augrain 
Date: Mon, 27 Aug 2018 19:12:28 +0200
Subject: [PATCH 08/56] allows compact definition lists

---
 browser/lib/markdown-it-deflist.js | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/browser/lib/markdown-it-deflist.js b/browser/lib/markdown-it-deflist.js
index 5dd02267..d0cdb656 100644
--- a/browser/lib/markdown-it-deflist.js
+++ b/browser/lib/markdown-it-deflist.js
@@ -46,10 +46,12 @@ module.exports = function definitionListPlugin (md) {
       listLines,
       listTokIdx,
       max,
+      newEndLine,
       nextLine,
       offset,
       oldDDIndent,
       oldIndent,
+      oldLineMax,
       oldParentType,
       oldSCount,
       oldTShift,
@@ -150,7 +152,16 @@ module.exports = function definitionListPlugin (md) {
         state.tight = true
         state.parentType = 'deflist'
 
-        state.md.block.tokenize(state, ddLine, endLine, true)
+        newEndLine = ddLine
+        while (++newEndLine < endLine && (state.sCount[newEndLine] >= state.sCount[ddLine] || state.isEmpty(newEndLine))) {
+        }
+        
+        oldLineMax = state.lineMax
+        state.lineMax = newEndLine
+        
+        state.md.block.tokenize(state, ddLine, newEndLine, true)
+        
+        state.lineMax = oldLineMax
 
         // If any of list item is tight, mark list as tight
         if (!state.tight || prevEmptyEnd) {

From f57c4f390d5546393f4473485f4f03281140be35 Mon Sep 17 00:00:00 2001
From: Baptiste Augrain 
Date: Mon, 27 Aug 2018 23:16:21 +0200
Subject: [PATCH 09/56] fix lint errors

---
 browser/lib/markdown-it-deflist.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/browser/lib/markdown-it-deflist.js b/browser/lib/markdown-it-deflist.js
index d0cdb656..f3c58009 100644
--- a/browser/lib/markdown-it-deflist.js
+++ b/browser/lib/markdown-it-deflist.js
@@ -155,12 +155,12 @@ module.exports = function definitionListPlugin (md) {
         newEndLine = ddLine
         while (++newEndLine < endLine && (state.sCount[newEndLine] >= state.sCount[ddLine] || state.isEmpty(newEndLine))) {
         }
-        
+
         oldLineMax = state.lineMax
         state.lineMax = newEndLine
-        
+
         state.md.block.tokenize(state, ddLine, newEndLine, true)
-        
+
         state.lineMax = oldLineMax
 
         // If any of list item is tight, mark list as tight

From 5006aaae38d409aab09d0b9b1bd73e19be09959b Mon Sep 17 00:00:00 2001
From: Baptiste Augrain 
Date: Tue, 28 Aug 2018 01:44:33 +0200
Subject: [PATCH 10/56] fix live note counts when multiple tags are selected

---
 browser/main/SideNav/index.js | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js
index 0dd34989..65f76691 100644
--- a/browser/main/SideNav/index.js
+++ b/browser/main/SideNav/index.js
@@ -19,8 +19,8 @@ import {SortableContainer} from 'react-sortable-hoc'
 import i18n from 'browser/lib/i18n'
 import context from 'browser/lib/context'
 
-function findOne (haystack, arr) {
-  return arr.some(v => haystack.indexOf(v) >= 0)
+function matchActiveTags (tags, activeTags) {
+  return _.every(activeTags, v => tags.indexOf(v) >= 0)
 }
 
 class SideNav extends React.Component {
@@ -158,7 +158,7 @@ class SideNav extends React.Component {
     if (config.ui.enableLiveNoteCounts && activeTags.length !== 0) {
       const notesTags = data.noteMap.map(note => note.tags)
       tagList = tagList.map(tag => {
-        tag.size = notesTags.filter(tags => tags.includes(tag.name) && findOne(tags, activeTags)).length
+        tag.size = notesTags.filter(tags => tags.includes(tag.name) && matchActiveTags(tags, activeTags)).length
         return tag
       })
     }

From 8b4a9dd3259b6935e9fc2c6b346ca62d0ee02cdb Mon Sep 17 00:00:00 2001
From: Baptiste Augrain 
Date: Wed, 29 Aug 2018 19:28:09 +0200
Subject: [PATCH 11/56] add `saveTagsAlphabetically` option

---
 browser/main/Detail/MarkdownNoteDetail.js     |  1 +
 browser/main/Detail/SnippetNoteDetail.js      |  1 +
 browser/main/Detail/TagSelect.js              | 10 ++++++++--
 browser/main/modals/PreferencesModal/UiTab.js | 12 ++++++++++++
 locales/en.json                               |  1 +
 locales/fr.json                               |  1 +
 6 files changed, 24 insertions(+), 2 deletions(-)

diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js
index 705736e9..1e30d28b 100755
--- a/browser/main/Detail/MarkdownNoteDetail.js
+++ b/browser/main/Detail/MarkdownNoteDetail.js
@@ -363,6 +363,7 @@ class MarkdownNoteDetail extends React.Component {
         
diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js
index 3633ec85..4a54e7c4 100644
--- a/browser/main/Detail/SnippetNoteDetail.js
+++ b/browser/main/Detail/SnippetNoteDetail.js
@@ -744,6 +744,7 @@ class SnippetNoteDetail extends React.Component {
          this.handleChange(e)}
         />
diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js
index e51d5673..9bfbabbe 100644
--- a/browser/main/Detail/TagSelect.js
+++ b/browser/main/Detail/TagSelect.js
@@ -82,8 +82,14 @@ class TagSelect extends React.Component {
     value = _.isArray(value)
       ? value.slice()
       : []
-    value.push(newTag)
-    value = _.uniq(value)
+
+    if (!_.includes(value, newTag)) {
+        value.push(newTag)
+    }
+
+    if (this.props.saveTagsAlphabetically) {
+      value = _.sortBy(value)
+    }
 
     this.setState({
       newTag: ''
diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js
index 40269190..a53281bc 100644
--- a/browser/main/modals/PreferencesModal/UiTab.js
+++ b/browser/main/modals/PreferencesModal/UiTab.js
@@ -71,6 +71,7 @@ class UiTab extends React.Component {
         confirmDeletion: this.refs.confirmDeletion.checked,
         showOnlyRelatedTags: this.refs.showOnlyRelatedTags.checked,
         showTagsAlphabetically: this.refs.showTagsAlphabetically.checked,
+        saveTagsAlphabetically: this.refs.saveTagsAlphabetically.checked,
         enableLiveNoteCounts: this.refs.enableLiveNoteCounts.checked,
         disableDirectWrite: this.refs.uiD2w != null
           ? this.refs.uiD2w.checked
@@ -244,6 +245,17 @@ class UiTab extends React.Component {
           }
           
Tags
+
+ +
+
- +