Repository: manusa/electronim
Pull request #33 (2020-08-23 18:41:04)

Feat/doc dialog

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=SECURITY_HOTSPOT) [2 Security Hotspots](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90.png' alt='96.3%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=33&metric=new_coverage&view=list) [96.3% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=33&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=33&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=33&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=VULNERABILITY) (and [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=SECURITY_HOTSPOT) [2 Security Hotspots](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=SECURITY_HOTSPOT) to review)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=manusa_electronim&pullRequest=33&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/90.png' alt='96.3%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=33&metric=new_coverage&view=list) [96.3% Coverage](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=33&metric=new_coverage&view=list)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=33&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=manusa_electronim&pullRequest=33&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/docs/Keyboard-shortcuts.md b/docs/Keyboard-shortcuts.md
index fac2dfb..de80ac7 100644
--- a/docs/Keyboard-shortcuts.md
+++ b/docs/Keyboard-shortcuts.md
@@ -1,8 +1,8 @@
 # Keyboard Shortcuts
 
-| Key combination                                           | Description         |
-|-----------------------------------------------------------|---------------------|
-| `Ctrl+r` `Cmd+r` `F5`                                     | Reload current tab. |
-| `Ctrl++` `Cmd++` <br/> `Ctrl+ScrollUp` `Cmd+ScrollUp`     | Zoom in.            |
-| `Ctrl+-` `Cmd+-` <br/> `Ctrl+ScrollDown` `Cmd+ScrollDown` | Zoom out.           |
-| `Ctrl+0` `Cmd+0`                                          | Reset zoom.         |
\ No newline at end of file
+| Key combination                                            | Description         |
+|------------------------------------------------------------|---------------------|
+| `Ctrl+r` `Cmd+r` `F5`                                      | Reload current tab. |
+| `Ctrl++` `Cmd++` <br /> `Ctrl+ScrollUp` `Cmd+ScrollUp`     | Zoom in.            |
+| `Ctrl+-` `Cmd+-` <br /> `Ctrl+ScrollDown` `Cmd+ScrollDown` | Zoom out.           |
+| `Ctrl+0` `Cmd+0`                                           | Reset zoom.         |
\ No newline at end of file
diff --git a/docs/Roadmap.md b/docs/Roadmap.md
index 214f5b3..33f3791 100644
--- a/docs/Roadmap.md
+++ b/docs/Roadmap.md
@@ -22,7 +22,7 @@
 - [ ] Better support for Snap ([#20](https://github.com/manusa/electronim/issues/20)...)
 - [ ] Embed dictionaries in repository // Pre-download dictionaries from original sources
       (Chromium, LibreOffice, etc.) 
-- [ ] Replace current templates to use [preact](https://github.com/preactjs/preact) instead
+- [x] Replace current templates to use [preact](https://github.com/preactjs/preact) instead
       ([#21](https://github.com/manusa/electronim/pull/21),
       [#31](https://github.com/manusa/electronim/pull/31))
 - [ ] ~~Evaluate migration to use template engines such as [lit-html](https://github.com/Polymer/lit-html)
diff --git a/package-lock.json b/package-lock.json
index 6b3eed3..8cb0fed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1209,7 +1209,6 @@
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
       "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
-      "dev": true,
       "requires": {
         "sprintf-js": "~1.0.2"
       },
@@ -1217,8 +1216,7 @@
         "sprintf-js": {
           "version": "1.0.3",
           "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
-          "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
-          "dev": true
+          "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
         }
       }
     },
@@ -2728,6 +2726,11 @@
         "ansi-colors": "^4.1.1"
       }
     },
+    "entities": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz",
+      "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ=="
+    },
     "env-paths": {
       "version": "2.2.0",
       "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.0.tgz",
@@ -5319,6 +5322,14 @@
       "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
       "dev": true
     },
+    "linkify-it": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.2.tgz",
+      "integrity": "sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==",
+      "requires": {
+        "uc.micro": "^1.0.1"
+      }
+    },
     "load-json-file": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
@@ -5420,6 +5431,18 @@
         "object-visit": "^1.0.0"
       }
     },
+    "markdown-it": {
+      "version": "11.0.0",
+      "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-11.0.0.tgz",
+      "integrity": "sha512-+CvOnmbSubmQFSA9dKz1BRiaSMV7rhexl3sngKqFyXSagoA3fBdJQ8oZWtRy2knXdpDXaBw44euz37DeJQ9asg==",
+      "requires": {
+        "argparse": "^1.0.7",
+        "entities": "~2.0.0",
+        "linkify-it": "^3.0.1",
+        "mdurl": "^1.0.1",
+        "uc.micro": "^1.0.5"
+      }
+    },
     "matcher": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz",
@@ -5429,6 +5452,11 @@
         "escape-string-regexp": "^4.0.0"
       }
     },
+    "mdurl": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
+      "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4="
+    },
     "merge-stream": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -7417,6 +7445,11 @@
         "is-typedarray": "^1.0.0"
       }
     },
+    "uc.micro": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
+      "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
+    },
     "unidragger": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/unidragger/-/unidragger-2.3.1.tgz",
diff --git a/package.json b/package.json
index 96e634d..313f5e9 100644
--- a/package.json
+++ b/package.json
@@ -174,6 +174,7 @@
     "dictionary-uk": "^2.1.1",
     "electron": "^9.1.2",
     "htm": "^3.0.4",
+    "markdown-it": "^11.0.0",
     "nodehun": "^3.0.1",
     "preact": "^10.4.5"
   }
diff --git a/src/browser-window/index.js b/src/browser-window/index.js
new file mode 100644
index 0000000..2aece89
--- /dev/null
+++ b/src/browser-window/index.js
@@ -0,0 +1,23 @@
+/*
+   Copyright 2019 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+const showDialog = (window, browserView) => {
+  window.setBrowserView(browserView);
+  const {width, height} = window.getContentBounds();
+  browserView.setBounds({x: 0, y: 0, width, height});
+  browserView.setAutoResize({width: true, horizontal: true, height: true, vertical: true});
+};
+
+module.exports = {showDialog};
diff --git a/src/chrome-tabs/index.js b/src/chrome-tabs/index.js
index 5691a05..cc2d547 100644
--- a/src/chrome-tabs/index.js
+++ b/src/chrome-tabs/index.js
@@ -14,6 +14,7 @@
    limitations under the License.
  */
 const {BrowserView, Menu, MenuItem} = require('electron');
+const {openHelpDialog} = require('../help');
 const {openSettingsDialog} = require('../settings');
 
 const TABS_CONTAINER_HEIGHT = 46;
@@ -27,6 +28,7 @@ const handleContextMenu = (mainWindow, browserView) => (event, params) => {
   const {webContents} = browserView;
   const menu = new Menu();
   menu.append(new MenuItem({label: 'Settings', click: () => openSettingsDialog(mainWindow)}));
+  menu.append(new MenuItem({label: 'Help', click: openHelpDialog(mainWindow)}));
   menu.append(new MenuItem({
     label: 'DevTools',
     click: () => webContents.openDevTools({mode: 'detach', activate: true})
diff --git a/src/constants/index.js b/src/constants/index.js
index 2f30849..68a9bc1 100644
--- a/src/constants/index.js
+++ b/src/constants/index.js
@@ -17,10 +17,10 @@ const APP_EVENTS = {
   activateTab: 'activateTab',
   activateTabInContainer: 'activateTabInContainer',
   addTabs: 'addTabs',
+  closeDialog: 'closeDialog',
   dictionaryGetMisspelled: 'dictionaryGetMisspelled',
   notificationClick: 'notificationClick',
   reload: 'reload',
-  settingsCancel: 'settingsCancel',
   settingsOpenDialog: 'settingsOpenDialog',
   settingsSave: 'settingsSave',
   setTabFavicon: 'setTabFavicon',
diff --git a/src/help/browser-help.css b/src/help/browser-help.css
new file mode 100644
index 0000000..cf80713
--- /dev/null
+++ b/src/help/browser-help.css
@@ -0,0 +1,118 @@
+/*
+   Copyright 2019 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+:root {
+  --default-spacing: 6px;
+  --border-color: #dbdbdb;
+  --toolbar-height: 60px;
+  overflow: hidden;
+}
+
+.help-root {
+  position: fixed;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  margin: 0;
+}
+
+.help-content {
+  position: absolute;
+  overflow: auto;
+  padding: calc(var(--default-spacing) * 2);
+  display: block;
+  top: 0;
+  bottom: var(--toolbar-height);
+  left: 0;
+  right: 0;
+}
+
+@media screen and (min-width:  768px) {
+  .help-content {
+    overflow: hidden;
+    padding: 0;
+    flex: 1;
+    display: flex;
+  }
+  .toc-container,
+  .documents-container {
+    overflow: auto;
+  }
+  .toc-container {
+    padding: calc(var(--default-spacing) * 2);
+    min-width: 250px;
+  }
+}
+
+.toc-container ol {
+  padding-left: 1.5rem;
+}
+
+.documents-container h1,
+.documents-container h2 {
+  color: #000;
+  padding-bottom: .3em;
+  border-bottom: 1px solid #eaecef;
+  margin-top: calc(var(--default-spacing) * 4);
+  margin-bottom: calc(var(--default-spacing) * 3);
+}
+
+.documents-container h1 {
+  font-size: 2rem;
+}
+
+.documents-container h2 {
+  font-size: 1.5rem;
+}
+
+.documents-container h3 {
+  color: #000;
+  font-size: 1.25rem;
+  margin-top: calc(var(--default-spacing) * 2);
+  margin-bottom: calc(var(--default-spacing) * 1);
+}
+
+.documents-container h4 {
+  font-weight: 600;
+}
+
+.documents-container p {
+  margin-block-start: 1rem;
+  margin-block-end: 1rem;
+}
+
+.documents-container ul {
+  list-style: disc;
+  padding-left: 2rem;
+}
+
+.documents-container blockquote {
+  padding: 0 1em;
+  color: #6a737d;
+  border-left: .25em solid #dfe2e5;
+}
+
+.toolbar {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  display: flex;
+  align-items: center;
+  justify-content: flex-end;
+  padding: var(--default-spacing);
+  height: var(--toolbar-height);
+}
\ No newline at end of file
diff --git a/src/help/browser-help.js b/src/help/browser-help.js
new file mode 100644
index 0000000..40690e8
--- /dev/null
+++ b/src/help/browser-help.js
@@ -0,0 +1,67 @@
+/*
+   Copyright 2019 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+const {APP_EVENTS, ipcRenderer, docs} = window;
+const {h, render} = window.preact;
+// const {useReducer} = window.preactHooks;
+const html = window.htm.bind(h);
+const helpRoot = () => document.querySelector('.help-root');
+
+const Document = ({id}) => html`
+  <a id=${id} />
+  <div dangerouslySetInnerHTML=${{
+    __html: docs[id]
+  }}></div>
+`;
+
+const Toc = () => html`
+  <div class="toc-container">
+    <h1 class="title">Table of Contents</h1>
+    <ol>
+      <li><a href="#Setup.md">Setup</a></li>
+      <li><a href="#Keyboard-shortcuts.md">Keyboard Shortcuts</a></li>
+      <li><a href="#Troubleshooting.md">Troubleshooting</a></li>
+    </ol>
+  </div>
+`;
+
+const Content = () => html`
+  <div class="documents-container">
+    <${Document} id="Setup.md"/>
+    <${Document} id="Keyboard-shortcuts.md"/>
+    <${Document} id="Troubleshooting.md"/>
+  </div>
+`;
+
+const Toolbar = () => {
+  const close = () => ipcRenderer.send(APP_EVENTS.closeDialog);
+  return html`
+    <div class="toolbar">
+      <button class="button is-link is-light" onclick=${close}>
+        close
+      </button>
+    </div>
+`;
+};
+
+const Help = () => html`
+    <div class="help-content">
+      <${Toc} />
+      <${Content} />
+    </div>
+    <${Toolbar} />
+  `;
+
+render(html`<${Help} />`, helpRoot());
diff --git a/src/help/index.html b/src/help/index.html
new file mode 100644
index 0000000..97d2065
--- /dev/null
+++ b/src/help/index.html
@@ -0,0 +1,30 @@
+<!--
+Copyright 2019 Marc Nuri San Felix
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <title>ElectronIM Help</title>
+  <link rel="stylesheet" href="../../node_modules/bulma/css/bulma.min.css">
+  <link rel="stylesheet" href="../../node_modules/@fortawesome/fontawesome-free/css/all.min.css">
+  <link rel="stylesheet" href="./browser-help.css">
+</head>
+<body>
+<div class="help-root"></div>
+<script src="./browser-help.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/src/help/index.js b/src/help/index.js
new file mode 100644
index 0000000..52108ce
--- /dev/null
+++ b/src/help/index.js
@@ -0,0 +1,52 @@
+/*
+   Copyright 2019 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+const {BrowserView} = require('electron');
+const fs = require('fs');
+const path = require('path');
+const md = require('markdown-it')({html: true, xhtmlOut: true});
+const {handleRedirect} = require('../tab-manager/redirect');
+const {showDialog} = require('../browser-window');
+
+const DOCS_DIR = path.resolve(__dirname, '../../docs');
+
+const webPreferences = {
+  preload: `${__dirname}/preload.js`
+};
+
+// Visible for testing
+const fixRelativeUrls = s => s.replace(
+  /((src|href)\s*?=\s*?['"](?!http))([^'"]+)(['"])/gi,
+  `$1${DOCS_DIR}/$3$4`
+);
+
+const loadDocs = () => fs.readdirSync(DOCS_DIR)
+  .filter(fileName => fileName.endsWith('.md'))
+  .reduce((acc, fileName) => {
+    acc[fileName] = fixRelativeUrls(md.render(fs.readFileSync(path.resolve(DOCS_DIR, fileName), 'utf8')));
+    return acc;
+  }, {});
+
+
+const openHelpDialog = mainWindow => () => {
+  const helpView = new BrowserView({webPreferences});
+  helpView.webContents.loadURL(`file://${__dirname}/index.html`);
+  const handleRedirectForCurrentUrl = handleRedirect(helpView);
+  helpView.webContents.on('will-navigate', handleRedirectForCurrentUrl);
+  helpView.webContents.on('new-window', handleRedirectForCurrentUrl);
+  showDialog(mainWindow, helpView);
+};
+
+module.exports = {fixRelativeUrls, openHelpDialog, loadDocs};
diff --git a/src/help/preload.js b/src/help/preload.js
new file mode 100644
index 0000000..8da6e99
--- /dev/null
+++ b/src/help/preload.js
@@ -0,0 +1,21 @@
+/*
+   Copyright 2019 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+require('../main/preload');
+window.preact = require('preact');
+window.preactHooks = require('preact/hooks');
+window.htm = require('htm');
+
+window.docs = require('./').loadDocs();
diff --git a/src/main/index.js b/src/main/index.js
index 838f70d..d737f2b 100644
--- a/src/main/index.js
+++ b/src/main/index.js
@@ -110,7 +110,7 @@ const initTabListener = () => {
   ipc.on(APP_EVENTS.zoomReset, handleZoomReset);
 };
 
-const closeSettings = () => {
+const closeDialog = () => {
   const settingsView = mainWindow.getBrowserView();
   activateTab(tabManager.getActiveTab());
   settingsView.destroy();
@@ -127,9 +127,9 @@ const saveSettings = (event, settings) => {
   viewsToDestroy.forEach(view => view.destroy());
 };
 
-const initSettingsListener = () => {
+const initDialogListeners = () => {
   ipc.on(APP_EVENTS.settingsSave, saveSettings);
-  ipc.on(APP_EVENTS.settingsCancel, closeSettings);
+  ipc.on(APP_EVENTS.closeDialog, closeDialog);
 };
 
 const browserVersionsReady = () => {
@@ -165,7 +165,7 @@ const init = () => {
   mainWindow.on('maximize', handleMainWindowResize);
   mainWindow.on('closed', () => app.quit());
   initTabListener();
-  initSettingsListener();
+  initDialogListeners();
   initBrowserVersions()
     .then(browserVersionsReady)
     .catch(() => {
diff --git a/src/settings/browser-settings.js b/src/settings/browser-settings.js
index 4921241..c81a6f2 100644
--- a/src/settings/browser-settings.js
+++ b/src/settings/browser-settings.js
@@ -238,7 +238,7 @@ const Settings = () => {
     enabledDictionaries,
     disableNotificationsGlobally: state.disableNotificationsGlobally
   });
-  const cancel = () => ipcRenderer.send(APP_EVENTS.settingsCancel);
+  const cancel = () => ipcRenderer.send(APP_EVENTS.closeDialog);
   return html`
   <h1 class="title">Settings</h1>
   <div class="container">
diff --git a/src/settings/index.js b/src/settings/index.js
index 1d79fd9..06b725f 100644
--- a/src/settings/index.js
+++ b/src/settings/index.js
@@ -17,6 +17,8 @@ const {BrowserView} = require('electron');
 const fs = require('fs');
 const path = require('path');
 const HOME_DIR = require('os').homedir();
+const {showDialog} = require('../browser-window');
+
 const APP_DIR = '.electronim';
 const SETTINGS_FILE = 'settings.json';
 const DEFAULT_SETTINGS = {tabs: [], enabledDictionaries: ['en-US']};
@@ -61,11 +63,8 @@ const updateSettings = settings =>
 
 const openSettingsDialog = mainWindow => {
   const settingsView = new BrowserView({webPreferences});
-  mainWindow.setBrowserView(settingsView);
-  const {width, height} = mainWindow.getContentBounds();
-  settingsView.setBounds({x: 0, y: 0, width, height});
-  settingsView.setAutoResize({width: true, horizontal: true, height: true, vertical: true});
   settingsView.webContents.loadURL(`file://${__dirname}/index.html`);
+  showDialog(mainWindow, settingsView);
 };
 
 module.exports = {loadSettings, updateSettings, openSettingsDialog};
diff --git a/src/tab-manager/index.js b/src/tab-manager/index.js
index 5f9de17..ed35d1e 100644
--- a/src/tab-manager/index.js
+++ b/src/tab-manager/index.js
@@ -17,8 +17,8 @@ const {app, BrowserView, Menu, MenuItem, session} = require('electron');
 const {APP_EVENTS} = require('../constants');
 const settings = require('../settings');
 const {contextMenuHandler} = require('../spell-check');
-const {handleRedirect} = require('./redirect');
 const {userAgentForView} = require('../user-agent');
+const {handleRedirect} = require('./redirect');
 
 let activeTab = null;
 const tabs = {};


Test patch:
diff --git a/src/browser-window/__tests/index.test.js b/src/browser-window/__tests/index.test.js
new file mode 100644
index 0000000..6426048
--- /dev/null
+++ b/src/browser-window/__tests/index.test.js
@@ -0,0 +1,42 @@
+/*
+   Copyright 2019 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+describe('browser-window util module test suite', () => {
+  let browserWindow;
+  beforeEach(() => {
+    browserWindow = require('../');
+  });
+  test('showDialog, should fill provided window with provided BrowserView', () => {
+    // Given
+    const window = {
+      getContentBounds: jest.fn(() => ({width: 13, height: 37})),
+      setBrowserView: jest.fn()
+    };
+    const dialog = {
+      setBounds: jest.fn(),
+      setAutoResize: jest.fn()
+    };
+    // When
+    browserWindow.showDialog(window, dialog);
+    // Then
+    expect(window.setBrowserView).toHaveBeenCalledWith(dialog);
+    expect(window.setBrowserView).toHaveBeenCalledBefore(dialog.setBounds);
+    expect(dialog.setBounds).toHaveBeenCalledTimes(1);
+    expect(dialog.setBounds).toHaveBeenCalledWith({x: 0, y: 0, width: 13, height: 37});
+    expect(dialog.setAutoResize).toHaveBeenCalledTimes(1);
+    expect(dialog.setAutoResize)
+      .toHaveBeenCalledWith({width: true, horizontal: true, height: true, vertical: true});
+  });
+});
diff --git a/src/help/__tests/browser-help.test.js b/src/help/__tests/browser-help.test.js
new file mode 100644
index 0000000..a4fe7dd
--- /dev/null
+++ b/src/help/__tests/browser-help.test.js
@@ -0,0 +1,62 @@
+/*
+   Copyright 2019 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+const {fireEvent} = require('@testing-library/dom');
+
+const mockDOM = () => {
+  document.body.innerHTML = '';
+  const $root = document.createElement('div');
+  $root.innerHTML = '<div class="help-root"></div>';
+  document.body.append($root);
+};
+
+describe('Help in Browser test suite', () => {
+  let mockIpcRenderer;
+  beforeEach(() => {
+    mockIpcRenderer = {
+      send: jest.fn()
+    };
+    window.preact = require('preact');
+    window.preactHooks = require('preact/hooks');
+    window.htm = require('htm');
+    window.APP_EVENTS = {closeDialog: 'close the dialog'};
+    window.docs = {
+      'Setup.md': '<span>Setup guide</span>',
+      'Keyboard-shortcuts.md': '<span>Keyboard shortcuts</span>',
+      'Troubleshooting.md': '<span>Troubleshooting</span>'
+    };
+    window.ipcRenderer = mockIpcRenderer;
+    mockDOM();
+    jest.isolateModules(() => {
+      require('../browser-help');
+    });
+  });
+  test('render, should render all documents', () => {
+    // Then
+    expect(document.querySelector('.toc-container').innerHTML)
+      .toMatch(/Table of Contents/);
+    expect(document.querySelector('.documents-container').innerHTML)
+      .toMatch(/Setup guide.+<span>Keyboard shortcuts.+Troubleshooting/);
+  });
+  describe('Main Button events', () => {
+    test('Close should send close dialog event', () => {
+      // When
+      fireEvent.click(document.querySelector('.button.is-link.is-light'));
+      // Then
+      expect(mockIpcRenderer.send).toHaveBeenCalledTimes(1);
+      expect(mockIpcRenderer.send).toHaveBeenCalledWith('close the dialog');
+    });
+  });
+});
diff --git a/src/help/__tests/index.test.js b/src/help/__tests/index.test.js
new file mode 100644
index 0000000..4590d59
--- /dev/null
+++ b/src/help/__tests/index.test.js
@@ -0,0 +1,73 @@
+/*
+   Copyright 2019 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+const path = require('path');
+const DOCS_DIR = path.resolve(__dirname, '../../../docs');
+describe('Help module test suite', () => {
+  let mockBrowserView;
+  let help;
+  beforeEach(() => {
+    mockBrowserView = {
+      loadURL: jest.fn()
+    };
+    jest.resetModules();
+    jest.mock('electron', () => ({
+      BrowserView: jest.fn(() => mockBrowserView)
+    }));
+    help = require('../');
+  });
+  describe('fixRelativeUrls', () => {
+    test('Combination of absolute and relative paths', () => {
+      // Given
+      const input = `
+        <img src\t   =  'relativePath'/>
+        <img src="https://absolute.com" /><a href="./relativeDir" />
+        <a href="http://test/some-path" />
+      `;
+      // When
+      const result = help.fixRelativeUrls(input);
+      // Then
+      expect(result).toBe(`
+        <img src	   =  '${DOCS_DIR}/relativePath'/>
+        <img src="https://absolute.com" /><a href="${DOCS_DIR}/./relativeDir" />
+        <a href="http://test/some-path" />
+      `);
+    });
+  });
+  test('loadDocs, should load object with documentation', () => {
+    // When
+    const docs = help.loadDocs();
+    // Then
+    expect(Object.keys(docs)).toContain('Setup.md');
+    expect(docs['Setup.md']).toMatch(/There are several options available/i);
+  });
+  test('openHelpDialog, should open dialog and add event listeners', () => {
+    // Given
+    mockBrowserView.setBounds = jest.fn();
+    mockBrowserView.setAutoResize = jest.fn();
+    mockBrowserView.webContents = {loadURL: jest.fn(), on: jest.fn()};
+    const mainWindow = {
+      getContentBounds: jest.fn(() => ({width: 13, height: 37})),
+      setBrowserView: jest.fn()
+    };
+    // When
+    help.openHelpDialog(mainWindow)();
+    // Then
+    expect(mockBrowserView.webContents.loadURL).toHaveBeenCalledTimes(1);
+    expect(mockBrowserView.webContents.loadURL).toHaveBeenCalledWith(expect.stringMatching(/.+?\/index.html$/));
+    expect(mockBrowserView.webContents.on).toHaveBeenCalledWith('will-navigate', expect.any(Function));
+  });
+});
+
diff --git a/src/help/__tests/preload.test.js b/src/help/__tests/preload.test.js
new file mode 100644
index 0000000..1427560
--- /dev/null
+++ b/src/help/__tests/preload.test.js
@@ -0,0 +1,34 @@
+/*
+   Copyright 2019 Marc Nuri San Felix
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+ */
+describe('Help Module preload test suite', () => {
+  let mockHelp;
+  beforeEach(() => {
+    mockHelp = {loadDocs: jest.fn()};
+    jest.resetModules();
+    jest.mock('../../main/preload', () => {
+      global.mainPreloadLoaded = true;
+    });
+    jest.mock('../', () => mockHelp);
+  });
+  test('preload', () => {
+    // Given
+    // When
+    require('../preload');
+    // Then
+    expect(global.mainPreloadLoaded).toBe(true);
+    expect(mockHelp.loadDocs).toHaveBeenCalledTimes(1);
+  });
+});
diff --git a/src/main/__tests__/index.test.js b/src/main/__tests__/index.test.js
index b4f7c12..28eee0b 100644
--- a/src/main/__tests__/index.test.js
+++ b/src/main/__tests__/index.test.js
@@ -343,7 +343,7 @@ describe('Main module test suite', () => {
       expect(settingsModule.openSettingsDialog).toHaveBeenCalledTimes(1);
     });
   });
-  describe('initSettingsListener ipc events', () => {
+  describe('initDialogListeners ipc events', () => {
     let settingsView;
     beforeEach(() => {
       settingsView = {destroy: jest.fn()};
@@ -365,11 +365,11 @@ describe('Main module test suite', () => {
       expect(settingsView.destroy).toHaveBeenCalledTimes(1);
       expect(mockTabContainer.destroy).toHaveBeenCalledTimes(1);
     });
-    test('closeSettings, should destroy settings view and activate current tab', () => {
+    test('closeDialog, should destroy dialog view and activate current tab', () => {
       // Given
       main.init();
       // When
-      mockIpc.listeners.settingsCancel();
+      mockIpc.listeners.closeDialog();
       // Then
       expect(settingsModule.updateSettings).not.toHaveBeenCalled();
       expect(tabManagerModule.getActiveTab).toHaveBeenCalledTimes(1);
diff --git a/src/settings/__tests__/browser-settings.test.js b/src/settings/__tests__/browser-settings.test.js
index f988d23..f18200d 100644
--- a/src/settings/__tests__/browser-settings.test.js
+++ b/src/settings/__tests__/browser-settings.test.js
@@ -73,9 +73,9 @@ describe('Settings in Browser test suite', () => {
           disableNotificationsGlobally: false
         });
     });
-    test('Cancel should send cancel event', () => {
+    test('Cancel should send close dialog event', () => {
       // Given
-      window.APP_EVENTS = {settingsCancel: 'Cancel my settings'};
+      window.APP_EVENTS = {closeDialog: 'Cancel my settings'};
       // When
       fireEvent.click(document.querySelector('.settings__cancel'));
       // Then
diff --git a/src/settings/__tests__/index.test.js b/src/settings/__tests__/index.test.js
index cf21a8d..451767c 100644
--- a/src/settings/__tests__/index.test.js
+++ b/src/settings/__tests__/index.test.js
@@ -132,11 +132,6 @@ describe('Settings module test suite', () => {
       // When
       settings.openSettingsDialog(mainWindow);
       // Then
-      expect(mockBrowserView.setBounds).toHaveBeenCalledTimes(1);
-      expect(mockBrowserView.setBounds).toHaveBeenCalledWith({x: 0, y: 0, width: 13, height: 37});
-      expect(mockBrowserView.setAutoResize).toHaveBeenCalledTimes(1);
-      expect(mockBrowserView.setAutoResize)
-        .toHaveBeenCalledWith({width: true, horizontal: true, height: true, vertical: true});
       expect(mockBrowserView.webContents.loadURL).toHaveBeenCalledTimes(1);
       expect(mockBrowserView.webContents.loadURL).toHaveBeenCalledWith(expect.stringMatching(/.+?\/index.html$/));
     });