Full Code of benweet/stackedit for AI

master 6dce2a5e36b7 cached
330 files
1005.7 KB
272.4k tokens
522 symbols
1 requests
Download .txt
Showing preview only (1,086K chars total). Download the full file or copy to clipboard to get everything.
Repository: benweet/stackedit
Branch: master
Commit: 6dce2a5e36b7
Files: 330
Total size: 1005.7 KB

Directory structure:
gitextract_i7x6xpbj/

├── .babelrc
├── .dockerignore
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── .stylelintrc
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── build/
│   ├── build.js
│   ├── check-versions.js
│   ├── deploy.sh
│   ├── dev-client.js
│   ├── dev-server.js
│   ├── utils.js
│   ├── vue-loader.conf.js
│   ├── webpack.base.conf.js
│   ├── webpack.dev.conf.js
│   ├── webpack.prod.conf.js
│   └── webpack.style.conf.js
├── chart/
│   ├── .helmignore
│   ├── Chart.yaml
│   ├── templates/
│   │   ├── NOTES.txt
│   │   ├── _helpers.tpl
│   │   ├── deployment.yaml
│   │   ├── ingress.yaml
│   │   ├── service.yaml
│   │   └── tests/
│   │       └── test-connection.yaml
│   └── values.yaml
├── chrome-app/
│   └── manifest.json
├── config/
│   ├── dev.env.js
│   ├── index.js
│   └── prod.env.js
├── gulpfile.js
├── index.html
├── index.js
├── package.json
├── server/
│   ├── conf.js
│   ├── github.js
│   ├── index.js
│   ├── pandoc.js
│   ├── pdf.js
│   └── user.js
├── src/
│   ├── components/
│   │   ├── App.vue
│   │   ├── ButtonBar.vue
│   │   ├── CodeEditor.vue
│   │   ├── ContextMenu.vue
│   │   ├── Editor.vue
│   │   ├── Explorer.vue
│   │   ├── ExplorerNode.vue
│   │   ├── FindReplace.vue
│   │   ├── Layout.vue
│   │   ├── Modal.vue
│   │   ├── NavigationBar.vue
│   │   ├── Notification.vue
│   │   ├── Preview.vue
│   │   ├── SideBar.vue
│   │   ├── SplashScreen.vue
│   │   ├── StatusBar.vue
│   │   ├── Toc.vue
│   │   ├── Tour.vue
│   │   ├── UserImage.vue
│   │   ├── UserName.vue
│   │   ├── common/
│   │   │   ├── EditorClassApplier.js
│   │   │   ├── PreviewClassApplier.js
│   │   │   └── vueGlobals.js
│   │   ├── gutters/
│   │   │   ├── Comment.vue
│   │   │   ├── CommentList.vue
│   │   │   ├── CurrentDiscussion.vue
│   │   │   ├── EditorNewDiscussionButton.vue
│   │   │   ├── NewComment.vue
│   │   │   ├── PreviewNewDiscussionButton.vue
│   │   │   └── StickyComment.vue
│   │   ├── menus/
│   │   │   ├── HistoryMenu.vue
│   │   │   ├── ImportExportMenu.vue
│   │   │   ├── MainMenu.vue
│   │   │   ├── PublishMenu.vue
│   │   │   ├── SyncMenu.vue
│   │   │   ├── WorkspaceBackupMenu.vue
│   │   │   ├── WorkspacesMenu.vue
│   │   │   └── common/
│   │   │       └── MenuEntry.vue
│   │   └── modals/
│   │       ├── AboutModal.vue
│   │       ├── AccountManagementModal.vue
│   │       ├── BadgeManagementModal.vue
│   │       ├── FilePropertiesModal.vue
│   │       ├── HtmlExportModal.vue
│   │       ├── ImageModal.vue
│   │       ├── LinkModal.vue
│   │       ├── PandocExportModal.vue
│   │       ├── PdfExportModal.vue
│   │       ├── PublishManagementModal.vue
│   │       ├── SettingsModal.vue
│   │       ├── SponsorModal.vue
│   │       ├── SyncManagementModal.vue
│   │       ├── TemplatesModal.vue
│   │       ├── WorkspaceManagementModal.vue
│   │       ├── common/
│   │       │   ├── FormEntry.vue
│   │       │   ├── ModalInner.vue
│   │       │   ├── Tab.vue
│   │       │   └── modalTemplate.js
│   │       └── providers/
│   │           ├── BloggerPagePublishModal.vue
│   │           ├── BloggerPublishModal.vue
│   │           ├── CouchdbCredentialsModal.vue
│   │           ├── CouchdbWorkspaceModal.vue
│   │           ├── DropboxAccountModal.vue
│   │           ├── DropboxPublishModal.vue
│   │           ├── DropboxSaveModal.vue
│   │           ├── GistPublishModal.vue
│   │           ├── GistSyncModal.vue
│   │           ├── GithubAccountModal.vue
│   │           ├── GithubOpenModal.vue
│   │           ├── GithubPublishModal.vue
│   │           ├── GithubSaveModal.vue
│   │           ├── GithubWorkspaceModal.vue
│   │           ├── GitlabAccountModal.vue
│   │           ├── GitlabOpenModal.vue
│   │           ├── GitlabPublishModal.vue
│   │           ├── GitlabSaveModal.vue
│   │           ├── GitlabWorkspaceModal.vue
│   │           ├── GoogleDriveAccountModal.vue
│   │           ├── GoogleDrivePublishModal.vue
│   │           ├── GoogleDriveSaveModal.vue
│   │           ├── GoogleDriveWorkspaceModal.vue
│   │           ├── GooglePhotoModal.vue
│   │           ├── WordpressPublishModal.vue
│   │           ├── ZendeskAccountModal.vue
│   │           └── ZendeskPublishModal.vue
│   ├── data/
│   │   ├── constants.js
│   │   ├── defaults/
│   │   │   ├── defaultLayoutSettings.js
│   │   │   ├── defaultLocalSettings.js
│   │   │   ├── defaultSettings.yml
│   │   │   └── defaultWorkspaces.js
│   │   ├── empties/
│   │   │   ├── emptyContent.js
│   │   │   ├── emptyContentState.js
│   │   │   ├── emptyFile.js
│   │   │   ├── emptyFolder.js
│   │   │   ├── emptyPublishLocation.js
│   │   │   ├── emptySyncLocation.js
│   │   │   ├── emptySyncedContent.js
│   │   │   ├── emptyTemplateHelpers.js
│   │   │   └── emptyTemplateValue.html
│   │   ├── faq.md
│   │   ├── features.js
│   │   ├── markdownSample.md
│   │   ├── pagedownButtons.js
│   │   ├── presets.js
│   │   ├── simpleModals.js
│   │   ├── templates/
│   │   │   ├── jekyllSiteTemplate.html
│   │   │   ├── plainHtmlTemplate.html
│   │   │   ├── styledHtmlTemplate.html
│   │   │   └── styledHtmlWithTocTemplate.html
│   │   └── welcomeFile.md
│   ├── extensions/
│   │   ├── abcExtension.js
│   │   ├── emojiExtension.js
│   │   ├── index.js
│   │   ├── katexExtension.js
│   │   ├── libs/
│   │   │   ├── markdownItAnchor.js
│   │   │   ├── markdownItMath.js
│   │   │   └── markdownItTasklist.js
│   │   ├── markdownExtension.js
│   │   └── mermaidExtension.js
│   ├── icons/
│   │   ├── Alert.vue
│   │   ├── ArrowLeft.vue
│   │   ├── CheckCircle.vue
│   │   ├── Close.vue
│   │   ├── CodeBraces.vue
│   │   ├── CodeTags.vue
│   │   ├── ContentCopy.vue
│   │   ├── ContentSave.vue
│   │   ├── Database.vue
│   │   ├── Delete.vue
│   │   ├── DotsHorizontal.vue
│   │   ├── Download.vue
│   │   ├── Eye.vue
│   │   ├── FileImage.vue
│   │   ├── FileMultiple.vue
│   │   ├── FilePlus.vue
│   │   ├── Folder.vue
│   │   ├── FolderMultiple.vue
│   │   ├── FolderPlus.vue
│   │   ├── FormatBold.vue
│   │   ├── FormatItalic.vue
│   │   ├── FormatListBulleted.vue
│   │   ├── FormatListChecks.vue
│   │   ├── FormatListNumbers.vue
│   │   ├── FormatQuoteClose.vue
│   │   ├── FormatSize.vue
│   │   ├── FormatStrikethrough.vue
│   │   ├── HelpCircle.vue
│   │   ├── History.vue
│   │   ├── Information.vue
│   │   ├── Key.vue
│   │   ├── LinkVariant.vue
│   │   ├── Login.vue
│   │   ├── Logout.vue
│   │   ├── Magnify.vue
│   │   ├── Menu.vue
│   │   ├── Message.vue
│   │   ├── NavigationBar.vue
│   │   ├── OpenInNew.vue
│   │   ├── Pen.vue
│   │   ├── Printer.vue
│   │   ├── Provider.vue
│   │   ├── Redo.vue
│   │   ├── ScrollSync.vue
│   │   ├── Seal.vue
│   │   ├── Settings.vue
│   │   ├── SidePreview.vue
│   │   ├── SignalOff.vue
│   │   ├── StatusBar.vue
│   │   ├── Sync.vue
│   │   ├── SyncOff.vue
│   │   ├── Table.vue
│   │   ├── Target.vue
│   │   ├── Toc.vue
│   │   ├── Undo.vue
│   │   ├── Upload.vue
│   │   ├── ViewList.vue
│   │   └── index.js
│   ├── index.js
│   ├── libs/
│   │   ├── clunderscore.js
│   │   ├── htmlSanitizer.js
│   │   └── pagedown.js
│   ├── services/
│   │   ├── animationSvc.js
│   │   ├── backupSvc.js
│   │   ├── badgeSvc.js
│   │   ├── diffUtils.js
│   │   ├── editor/
│   │   │   ├── cledit/
│   │   │   │   ├── cleditCore.js
│   │   │   │   ├── cleditHighlighter.js
│   │   │   │   ├── cleditKeystroke.js
│   │   │   │   ├── cleditMarker.js
│   │   │   │   ├── cleditSelectionMgr.js
│   │   │   │   ├── cleditUndoMgr.js
│   │   │   │   ├── cleditUtils.js
│   │   │   │   ├── cleditWatcher.js
│   │   │   │   └── index.js
│   │   │   ├── editorSvcDiscussions.js
│   │   │   ├── editorSvcUtils.js
│   │   │   └── sectionUtils.js
│   │   ├── editorSvc.js
│   │   ├── explorerSvc.js
│   │   ├── exportSvc.js
│   │   ├── extensionSvc.js
│   │   ├── gitWorkspaceSvc.js
│   │   ├── localDbSvc.js
│   │   ├── markdownConversionSvc.js
│   │   ├── markdownGrammarSvc.js
│   │   ├── networkSvc.js
│   │   ├── optional/
│   │   │   ├── index.js
│   │   │   ├── keystrokes.js
│   │   │   ├── scrollSync.js
│   │   │   ├── shortcuts.js
│   │   │   └── taskChange.js
│   │   ├── providers/
│   │   │   ├── bloggerPageProvider.js
│   │   │   ├── bloggerProvider.js
│   │   │   ├── common/
│   │   │   │   ├── Provider.js
│   │   │   │   └── providerRegistry.js
│   │   │   ├── couchdbWorkspaceProvider.js
│   │   │   ├── dropboxProvider.js
│   │   │   ├── gistProvider.js
│   │   │   ├── githubProvider.js
│   │   │   ├── githubWorkspaceProvider.js
│   │   │   ├── gitlabProvider.js
│   │   │   ├── gitlabWorkspaceProvider.js
│   │   │   ├── googleDriveAppDataProvider.js
│   │   │   ├── googleDriveProvider.js
│   │   │   ├── googleDriveWorkspaceProvider.js
│   │   │   ├── helpers/
│   │   │   │   ├── couchdbHelper.js
│   │   │   │   ├── dropboxHelper.js
│   │   │   │   ├── githubHelper.js
│   │   │   │   ├── gitlabHelper.js
│   │   │   │   ├── googleHelper.js
│   │   │   │   ├── wordpressHelper.js
│   │   │   │   └── zendeskHelper.js
│   │   │   ├── wordpressProvider.js
│   │   │   └── zendeskProvider.js
│   │   ├── publishSvc.js
│   │   ├── syncSvc.js
│   │   ├── tempFileSvc.js
│   │   ├── templateWorker.js
│   │   ├── timeSvc.js
│   │   ├── userSvc.js
│   │   ├── utils.js
│   │   └── workspaceSvc.js
│   ├── store/
│   │   ├── content.js
│   │   ├── contentState.js
│   │   ├── contextMenu.js
│   │   ├── data.js
│   │   ├── discussion.js
│   │   ├── explorer.js
│   │   ├── file.js
│   │   ├── findReplace.js
│   │   ├── folder.js
│   │   ├── index.js
│   │   ├── layout.js
│   │   ├── locationTemplate.js
│   │   ├── modal.js
│   │   ├── moduleTemplate.js
│   │   ├── notification.js
│   │   ├── queue.js
│   │   ├── syncedContent.js
│   │   ├── userInfo.js
│   │   └── workspace.js
│   └── styles/
│       ├── app.scss
│       ├── base.scss
│       ├── fonts.scss
│       ├── index.js
│       ├── markdownHighlighting.scss
│       ├── prism.scss
│       └── variables.scss
├── static/
│   ├── landing/
│   │   └── index.html
│   ├── oauth2/
│   │   └── callback.html
│   └── sitemap.xml
└── test/
    └── unit/
        ├── .eslintrc
        ├── jest.conf.js
        ├── mocks/
        │   ├── cryptoMock.js
        │   ├── localStorageMock.js
        │   ├── mutationObserverMock.js
        │   └── templateWorkerMock.js
        ├── setup.js
        └── specs/
            ├── components/
            │   ├── ButtonBar.spec.js
            │   ├── ContextMenu.spec.js
            │   ├── Explorer.spec.js
            │   ├── ExplorerNode.spec.js
            │   ├── NavigationBar.spec.js
            │   └── Notification.spec.js
            └── specUtils.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .babelrc
================================================
{
  "presets": [
    ["env", { "modules": false }],
    "stage-2"
  ],
  "plugins": ["transform-runtime"],
  "comments": false,
  "env": {
    "test": {
      "presets": ["env", "stage-2"],
      "plugins": ["transform-es2015-modules-commonjs", "dynamic-import-node"]
    }
  }
}


================================================
FILE: .dockerignore
================================================
node_modules
.git
dist
.history


================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true


================================================
FILE: .eslintignore
================================================
build/*.js
config/*.js
src/libs/*.js


================================================
FILE: .eslintrc.js
================================================
// http://eslint.org/docs/user-guide/configuring

module.exports = {
  root: true,
  parser: 'babel-eslint',
  parserOptions: {
    sourceType: 'module'
  },
  env: {
    browser: true,
  },
  extends: 'airbnb-base',
  // required to lint *.vue files
  plugins: [
    'html'
  ],
  globals: {
    "NODE_ENV": false,
    "VERSION": false
  },
  // check if imports actually resolve
  'settings': {
    'import/resolver': {
      'webpack': {
        'config': 'build/webpack.base.conf.js'
      }
    }
  },
  // add your custom rules here
  'rules': {
    'no-param-reassign': [2, { 'props': false }],
    // don't require .vue extension when importing
    'import/extensions': ['error', 'always', {
      'js': 'never',
      'vue': 'never'
    }],
    // allow optionalDependencies
    'import/no-extraneous-dependencies': ['error', {
      'optionalDependencies': ['test/unit/index.js']
    }],
    // allow debugger during development
    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
  }
}


================================================
FILE: .gitignore
================================================
.DS_Store
node_modules/
dist/
.history
.idea
npm-debug.log*
.vscode
stackedit_v4
chrome-app/*.zip
/test/unit/coverage/


================================================
FILE: .postcssrc.js
================================================
// https://github.com/michael-ciniawsky/postcss-load-config

module.exports = {
  "plugins": {
    // to edit target browsers: use "browserlist" field in package.json
    "autoprefixer": {}
  }
}


================================================
FILE: .stylelintrc
================================================
{
  "processors": ["stylelint-processor-html"],
  "extends": "stylelint-config-standard",
  "rules": {
    "no-empty-source": null
  }
}

================================================
FILE: .travis.yml
================================================
language: node_js

node_js:
  - "12"

services:
  - docker

before_deploy:
  # Run docker build
  - docker build -t benweet/stackedit .
  # Install Helm
  - curl -SL -o /tmp/get_helm.sh https://git.io/get_helm.sh
  - chmod 700 /tmp/get_helm.sh
  - /tmp/get_helm.sh
  - helm init --client-only

deploy:
  provider: script
  script: bash build/deploy.sh
  on:
    tags: true


================================================
FILE: Dockerfile
================================================
FROM benweet/stackedit-base

RUN mkdir -p /opt/stackedit
WORKDIR /opt/stackedit

COPY package*json /opt/stackedit/
COPY gulpfile.js /opt/stackedit/
RUN npm install --unsafe-perm \
  && npm cache clean --force
COPY . /opt/stackedit
ENV NODE_ENV production
RUN npm run build

EXPOSE 8080

CMD [ "node", "." ]


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "{}"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright {yyyy} {name of copyright owner}

   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.


================================================
FILE: README.md
================================================
# StackEdit

[![Build Status](https://img.shields.io/travis/benweet/stackedit.svg?style=flat)](https://travis-ci.org/benweet/stackedit) [![NPM version](https://img.shields.io/npm/v/stackedit.svg?style=flat)](https://www.npmjs.org/package/stackedit)

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

https://stackedit.io/

### Ecosystem

- [Chrome app](https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg)
- NEW! Embed StackEdit in any website with [stackedit.js](https://github.com/benweet/stackedit.js)
- NEW! [Chrome extension](https://chrome.google.com/webstore/detail/ajehldoplanpchfokmeempkekhnhmoha) that uses stackedit.js
- [Community](https://community.stackedit.io/)

### Build

```bash
# install dependencies
npm install

# serve with hot reload at localhost:8080
npm start

# build for production with minification
npm run build

# build for production and view the bundle analyzer report
npm run build --report
```

### Deploy with Helm

StackEdit Helm chart allows easy StackEdit deployment to any Kubernetes cluster.
You can use it to configure deployment with your existing ingress controller and cert-manager.

```bash
# Add the StackEdit Helm repository
helm repo add stackedit https://benweet.github.io/stackedit-charts/

# Update your local Helm chart repository cache
helm repo update

# Deploy StackEdit chart to your cluster
helm install --name stackedit stackedit/stackedit \
  --set dropboxAppKey=$DROPBOX_API_KEY \
  --set dropboxAppKeyFull=$DROPBOX_FULL_ACCESS_API_KEY \
  --set googleClientId=$GOOGLE_CLIENT_ID \
  --set googleApiKey=$GOOGLE_API_KEY \
  --set githubClientId=$GITHUB_CLIENT_ID \
  --set githubClientSecret=$GITHUB_CLIENT_SECRET \
  --set wordpressClientId=\"$WORDPRESS_CLIENT_ID\" \
  --set wordpressSecret=$WORDPRESS_CLIENT_SECRET
```

Later, to upgrade StackEdit to the latest version:

```bash
helm repo update
helm upgrade stackedit stackedit/stackedit
```

If you want to uninstall StackEdit:

```bash
helm delete --purge stackedit
```

If you want to use your existing ingress controller and cert-manager issuer:

```bash
# See https://docs.cert-manager.io/en/latest/tutorials/acme/quick-start/index.html
helm install --name stackedit stackedit/stackedit \
  --set dropboxAppKey=$DROPBOX_API_KEY \
  --set dropboxAppKeyFull=$DROPBOX_FULL_ACCESS_API_KEY \
  --set googleClientId=$GOOGLE_CLIENT_ID \
  --set googleApiKey=$GOOGLE_API_KEY \
  --set githubClientId=$GITHUB_CLIENT_ID \
  --set githubClientSecret=$GITHUB_CLIENT_SECRET \
  --set wordpressClientId=\"$WORDPRESS_CLIENT_ID\" \
  --set wordpressSecret=$WORDPRESS_CLIENT_SECRET \
  --set ingress.enabled=true \
  --set ingress.annotations."kubernetes\.io/ingress\.class"=nginx \
  --set ingress.annotations."cert-manager\.io/cluster-issuer"=letsencrypt-prod \
  --set ingress.hosts[0].host=stackedit.example.com \
  --set ingress.hosts[0].paths[0]=/ \
  --set ingress.tls[0].secretName=stackedit-tls \
  --set ingress.tls[0].hosts[0]=stackedit.example.com
```


================================================
FILE: build/build.js
================================================
require('./check-versions')()

process.env.NODE_ENV = 'production'

var ora = require('ora')
var rm = require('rimraf')
var path = require('path')
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('../config')
var webpackConfig = require('./webpack.prod.conf')

var spinner = ora('building for production...')
spinner.start()

rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
  if (err) throw err
  webpack(webpackConfig, function (err, stats) {
    spinner.stop()
    if (err) throw err
    process.stdout.write(stats.toString({
      colors: true,
      modules: false,
      children: false,
      chunks: false,
      chunkModules: false
    }) + '\n\n')

    console.log(chalk.cyan('  Build complete.\n'))
    console.log(chalk.yellow(
      '  Tip: built files are meant to be served over an HTTP server.\n' +
      '  Opening index.html over file:// won\'t work.\n'
    ))
  })
})


================================================
FILE: build/check-versions.js
================================================
var chalk = require('chalk')
var semver = require('semver')
var packageConfig = require('../package.json')
var shell = require('shelljs')
function exec (cmd) {
  return require('child_process').execSync(cmd).toString().trim()
}

var versionRequirements = [
  {
    name: 'node',
    currentVersion: semver.clean(process.version),
    versionRequirement: packageConfig.engines.node
  },
]

if (shell.which('npm')) {
  versionRequirements.push({
    name: 'npm',
    currentVersion: exec('npm --version'),
    versionRequirement: packageConfig.engines.npm
  })
}

module.exports = function () {
  var warnings = []
  for (var i = 0; i < versionRequirements.length; i++) {
    var mod = versionRequirements[i]
    if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
      warnings.push(mod.name + ': ' +
        chalk.red(mod.currentVersion) + ' should be ' +
        chalk.green(mod.versionRequirement)
      )
    }
  }

  if (warnings.length) {
    console.log('')
    console.log(chalk.yellow('To use this template, you must update following to modules:'))
    console.log()
    for (var i = 0; i < warnings.length; i++) {
      var warning = warnings[i]
      console.log('  ' + warning)
    }
    console.log()
    process.exit(1)
  }
}


================================================
FILE: build/deploy.sh
================================================
#!/bin/bash
set -e

# Tag and push docker image
docker login -u benweet -p "$DOCKER_PASSWORD"
docker tag benweet/stackedit "benweet/stackedit:$TRAVIS_TAG"
docker push benweet/stackedit:$TRAVIS_TAG
docker tag benweet/stackedit:$TRAVIS_TAG benweet/stackedit:latest
docker push benweet/stackedit:latest

# Build the chart
cd "$TRAVIS_BUILD_DIR"
npm run chart

# Add chart to helm repository
git clone --branch master "https://benweet:$GITHUB_TOKEN@github.com/benweet/stackedit-charts.git" /tmp/charts
cd /tmp/charts
helm package "$TRAVIS_BUILD_DIR/dist/stackedit"
helm repo index --url https://benweet.github.io/stackedit-charts/ .
git config user.name "Benoit Schweblin"
git config user.email "benoit.schweblin@gmail.com"
git add .
git commit -m "Added $TRAVIS_TAG"
git push origin master


================================================
FILE: build/dev-client.js
================================================
/* eslint-disable */
require('eventsource-polyfill')
var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')

hotClient.subscribe(function (event) {
  if (event.action === 'reload') {
    window.location.reload()
  }
})


================================================
FILE: build/dev-server.js
================================================
require('./check-versions')()

var config = require('../config')
Object.keys(config.dev.env).forEach((key) => {
  if (!process.env[key]) {
    process.env[key] = JSON.parse(config.dev.env[key]);
  }
});

var opn = require('opn')
var path = require('path')
var express = require('express')
var webpack = require('webpack')
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = require('./webpack.dev.conf')

// default port where dev server listens for incoming traffic
var port = process.env.PORT || config.dev.port
// automatically open browser, if not set will be false
var autoOpenBrowser = !!config.dev.autoOpenBrowser
// Define HTTP proxies to your custom API backend
// https://github.com/chimurai/http-proxy-middleware
var proxyTable = config.dev.proxyTable

var app = express()
var compiler = webpack(webpackConfig)

// StackEdit custom middlewares
require('../server')(app);

var devMiddleware = require('webpack-dev-middleware')(compiler, {
  publicPath: webpackConfig.output.publicPath,
  quiet: true
})

var hotMiddleware = require('webpack-hot-middleware')(compiler, {
  log: () => {}
})
// force page reload when html-webpack-plugin template changes
compiler.plugin('compilation', function (compilation) {
  compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
    hotMiddleware.publish({ action: 'reload' })
    cb()
  })
})

// proxy api requests
Object.keys(proxyTable).forEach(function (context) {
  var options = proxyTable[context]
  if (typeof options === 'string') {
    options = { target: options }
  }
  app.use(proxyMiddleware(options.filter || context, options))
})

// handle fallback for HTML5 history API
app.use(require('connect-history-api-fallback')())

// serve webpack bundle output
app.use(devMiddleware)

// enable hot-reload and state-preserving
// compilation error display
app.use(hotMiddleware)

// serve pure static assets
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))

var uri = 'http://localhost:' + port

var _resolve
var readyPromise = new Promise(resolve => {
  _resolve = resolve
})

console.log('> Starting dev server...')
devMiddleware.waitUntilValid(() => {
  console.log('> Listening at ' + uri + '\n')
  // when env is testing, don't need open it
  if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
    opn(uri)
  }
  _resolve()
})

var server = app.listen(port)

module.exports = {
  ready: readyPromise,
  close: () => {
    server.close()
  }
}


================================================
FILE: build/utils.js
================================================
var path = require('path')
var config = require('../config')
var ExtractTextPlugin = require('extract-text-webpack-plugin')

exports.assetsPath = function (_path) {
  var assetsSubDirectory = process.env.NODE_ENV === 'production'
    ? config.build.assetsSubDirectory
    : config.dev.assetsSubDirectory
  return path.posix.join(assetsSubDirectory, _path)
}

exports.cssLoaders = function (options) {
  options = options || {}

  var cssLoader = {
    loader: 'css-loader',
    options: {
      minimize: process.env.NODE_ENV === 'production',
      sourceMap: options.sourceMap
    }
  }

  // generate loader string to be used with extract text plugin
  function generateLoaders (loader, loaderOptions) {
    var loaders = [cssLoader]
    if (loader) {
      loaders.push({
        loader: loader + '-loader',
        options: Object.assign({}, loaderOptions, {
          sourceMap: options.sourceMap
        })
      })
    }

    // Extract CSS when that option is specified
    // (which is the case during production build)
    if (options.extract) {
      return ExtractTextPlugin.extract({
        use: loaders,
        fallback: 'vue-style-loader'
      })
    } else {
      return ['vue-style-loader'].concat(loaders)
    }
  }

  // https://vue-loader.vuejs.org/en/configurations/extract-css.html
  return {
    css: generateLoaders(),
    postcss: generateLoaders(),
    less: generateLoaders('less'),
    sass: generateLoaders('sass', { indentedSyntax: true }),
    scss: generateLoaders('sass'),
    stylus: generateLoaders('stylus'),
    styl: generateLoaders('stylus')
  }
}

// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
  var output = []
  var loaders = exports.cssLoaders(options)
  for (var extension in loaders) {
    var loader = loaders[extension]
    output.push({
      test: new RegExp('\\.' + extension + '$'),
      use: loader
    })
  }
  return output
}


================================================
FILE: build/vue-loader.conf.js
================================================
var utils = require('./utils')
var config = require('../config')
var isProduction = process.env.NODE_ENV === 'production'

module.exports = {
  loaders: utils.cssLoaders({
    sourceMap: isProduction
      ? config.build.productionSourceMap
      : config.dev.cssSourceMap,
    extract: isProduction
  })
}


================================================
FILE: build/webpack.base.conf.js
================================================
var path = require('path')
var webpack = require('webpack')
var utils = require('./utils')
var config = require('../config')
var VueLoaderPlugin = require('vue-loader/lib/plugin')
var vueLoaderConfig = require('./vue-loader.conf')
var StylelintPlugin = require('stylelint-webpack-plugin')

function resolve (dir) {
  return path.join(__dirname, '..', dir)
}

module.exports = {
  entry: {
    app: './src/'
  },
  node: {
    // For mermaid
    fs: 'empty' // jison generated code requires 'fs'
  },
  output: {
    path: config.build.assetsRoot,
    filename: '[name].js',
    publicPath: process.env.NODE_ENV === 'production'
      ? config.build.assetsPublicPath
      : config.dev.assetsPublicPath
  },
  resolve: {
    extensions: ['.js', '.vue', '.json'],
    alias: {
      '@': resolve('src')
    }
  },
  module: {
    rules: [
      {
        test: /\.(js|vue)$/,
        loader: 'eslint-loader',
        enforce: 'pre',
        include: [resolve('src'), resolve('test')],
        options: {
          formatter: require('eslint-friendly-formatter')
        }
      },
      {
        test: /\.vue$/,
        loader: 'vue-loader',
        options: vueLoaderConfig
      },
      // We can't pass graphlibrary to babel
      {
        test: /\.js$/,
        loader: 'string-replace-loader',
        include: [
          resolve('node_modules/graphlibrary')
        ],
        options: {
          search: '^\\s*(?:let|const) ',
          replace: 'var ',
          flags: 'gm'
        }
      },
      {
        test: /\.js$/,
        loader: 'babel-loader',
        include: [
          resolve('src'),
          resolve('test'),
          resolve('node_modules/mermaid')
        ],
        exclude: [
          resolve('node_modules/mermaid/src/diagrams/class/parser'),
          resolve('node_modules/mermaid/src/diagrams/flowchart/parser'),
          resolve('node_modules/mermaid/src/diagrams/gantt/parser'),
          resolve('node_modules/mermaid/src/diagrams/git/parser'),
          resolve('node_modules/mermaid/src/diagrams/sequence/parser')
        ],
      },
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: 'url-loader',
        options: {
          limit: 10000,
          name: utils.assetsPath('img/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(ttf|eot|otf|woff2?)(\?.*)?$/,
        loader: 'file-loader',
        options: {
          name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
        }
      },
      {
        test: /\.(md|yml|html)$/,
        loader: 'raw-loader'
      }
    ]
  },
  plugins: [
    new VueLoaderPlugin(),
    new StylelintPlugin({
      files: ['**/*.vue', '**/*.scss']
    }),
    new webpack.DefinePlugin({
      VERSION: JSON.stringify(require('../package.json').version)
    })
  ]
}


================================================
FILE: build/webpack.dev.conf.js
================================================
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')

// add hot-reload related code to entry chunks
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
  baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})

module.exports = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
  },
  // cheap-module-eval-source-map is faster for development
  devtool: 'source-map',
  plugins: [
    new webpack.DefinePlugin({
      NODE_ENV: config.dev.env.NODE_ENV
    }),
    // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),
    // https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({
      filename: 'index.html',
      template: 'index.html',
      inject: true
    }),
    new FriendlyErrorsPlugin()
  ]
})


================================================
FILE: build/webpack.prod.conf.js
================================================
var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
var OfflinePlugin = require('offline-plugin');
var WebpackPwaManifest = require('webpack-pwa-manifest')
var FaviconsWebpackPlugin = require('favicons-webpack-plugin')

function resolve (dir) {
  return path.join(__dirname, '..', dir)
}

var env = config.build.env

var webpackConfig = merge(baseWebpackConfig, {
  module: {
    rules: utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true
    })
  },
  devtool: config.build.productionSourceMap ? '#source-map' : false,
  output: {
    path: config.build.assetsRoot,
    filename: utils.assetsPath('js/[name].[chunkhash].js'),
    chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
  },
  plugins: [
    // http://vuejs.github.io/vue-loader/en/workflow/production.html
    new webpack.DefinePlugin({
      NODE_ENV: env.NODE_ENV,
      GOOGLE_CLIENT_ID: env.GOOGLE_CLIENT_ID,
      GITHUB_CLIENT_ID: env.GITHUB_CLIENT_ID
    }),
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      },
      sourceMap: true
    }),
    // extract css into its own file
    new ExtractTextPlugin({
      filename: utils.assetsPath('css/[name].[contenthash].css')
    }),
    // Compress extracted CSS. We are using this plugin so that possible
    // duplicated CSS from different components can be deduped.
    new OptimizeCSSPlugin({
      cssProcessorOptions: {
        safe: true
      }
    }),
    // generate dist index.html with correct asset hash for caching.
    // you can customize output by editing /index.html
    // see https://github.com/ampedandwired/html-webpack-plugin
    new HtmlWebpackPlugin({
      filename: config.build.index,
      template: 'index.html',
      inject: true,
      minify: {
        removeComments: true,
        collapseWhitespace: true,
        removeAttributeQuotes: true
        // more options:
        // https://github.com/kangax/html-minifier#options-quick-reference
      },
      // necessary to consistently work with multiple chunks via CommonsChunkPlugin
      chunksSortMode: 'dependency'
    }),
    // split vendor js into its own file
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function (module, count) {
        // any required modules inside node_modules are extracted to vendor
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
        )
      }
    }),
    // extract webpack runtime and module manifest to its own file in order to
    // prevent vendor hash from being updated whenever app bundle is updated
    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest',
      chunks: ['vendor']
    }),
    // copy custom static assets
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, '../static'),
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
      }
    ]),
    new FaviconsWebpackPlugin({
      logo: resolve('src/assets/favicon.png'),
      title: 'StackEdit',
    }),
    new WebpackPwaManifest({
      name: 'StackEdit',
      description: 'Full-featured, open-source Markdown editor',
      display: 'standalone',
      orientation: 'any',
      start_url: 'app',
      background_color: '#ffffff',
      crossorigin: 'use-credentials',
      icons: [{
        src: resolve('src/assets/favicon.png'),
        sizes: [96, 128, 192, 256, 384, 512]
      }]
    }),
    new OfflinePlugin({
      ServiceWorker: {
        events: true
      },
      AppCache: true,
      excludes: ['**/.*', '**/*.map', '**/index.html', '**/static/oauth2/callback.html', '**/icons-*/*.png', '**/static/fonts/KaTeX_*'],
      externals: ['/', '/app', '/oauth2/callback']
    }),
  ]
})

if (config.build.productionGzip) {
  var CompressionWebpackPlugin = require('compression-webpack-plugin')

  webpackConfig.plugins.push(
    new CompressionWebpackPlugin({
      asset: '[path].gz[query]',
      algorithm: 'gzip',
      test: new RegExp(
        '\\.(' +
        config.build.productionGzipExtensions.join('|') +
        ')$'
      ),
      threshold: 10240,
      minRatio: 0.8
    })
  )
}

if (config.build.bundleAnalyzerReport) {
  var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
  webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}

module.exports = webpackConfig


================================================
FILE: build/webpack.style.conf.js
================================================
var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var utils = require('./utils')
var config = require('../config')
var vueLoaderConfig = require('./vue-loader.conf')
var StylelintPlugin = require('stylelint-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')

function resolve (dir) {
  return path.join(__dirname, '..', dir)
}

module.exports = {
  entry: {
    style: './src/styles/'
  },
  module: {
    rules: [{
      test: /\.(ttf|eot|otf|woff2?)(\?.*)?$/,
      loader: 'file-loader',
      options: {
        name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
      }
    }]
    .concat(utils.styleLoaders({
      sourceMap: config.build.productionSourceMap,
      extract: true
    })),
  },
  output: {
    path: config.build.assetsRoot,
    filename: '[name].js',
    publicPath: config.build.assetsPublicPath
  },
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        warnings: false
      },
      sourceMap: true
    }),
    // extract css into its own file
    new ExtractTextPlugin({
      filename: '[name].css',
    }),
    // Compress extracted CSS. We are using this plugin so that possible
    // duplicated CSS from different components can be deduped.
    new OptimizeCSSPlugin({
      cssProcessorOptions: {
        safe: true
      }
    }),
  ]
}


================================================
FILE: chart/.helmignore
================================================
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/


================================================
FILE: chart/Chart.yaml
================================================
apiVersion: v1
appVersion: vSTACKEDIT_VERSION
description: In-browser Markdown editor
name: stackedit
version: STACKEDIT_VERSION


================================================
FILE: chart/templates/NOTES.txt
================================================
1. Get the application URL by running these commands:
{{- if .Values.ingress.enabled }}
{{- range $host := .Values.ingress.hosts }}
  {{- range .paths }}
  http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
  {{- end }}
{{- end }}
{{- else if contains "NodePort" .Values.service.type }}
  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "stackedit.fullname" . }})
  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$NODE_IP:$NODE_PORT
{{- else if contains "LoadBalancer" .Values.service.type }}
     NOTE: It may take a few minutes for the LoadBalancer IP to be available.
           You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "stackedit.fullname" . }}'
  export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "stackedit.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
  echo http://$SERVICE_IP:{{ .Values.service.port }}
{{- else if contains "ClusterIP" .Values.service.type }}
  export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "stackedit.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}")
  echo "Visit http://127.0.0.1:8080 to use your application"
  kubectl port-forward $POD_NAME 8080:80
{{- end }}


================================================
FILE: chart/templates/_helpers.tpl
================================================
{{/* vim: set filetype=mustache: */}}
{{/*
Expand the name of the chart.
*/}}
{{- define "stackedit.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{/*
Create a default fully qualified app name.
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
If release name contains chart name it will be used as a full name.
*/}}
{{- define "stackedit.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- $name := default .Chart.Name .Values.nameOverride -}}
{{- if contains $name .Release.Name -}}
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- end -}}

{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "stackedit.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
{{- end -}}

{{/*
Common labels
*/}}
{{- define "stackedit.labels" -}}
app.kubernetes.io/name: {{ include "stackedit.name" . }}
helm.sh/chart: {{ include "stackedit.chart" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}


================================================
FILE: chart/templates/deployment.yaml
================================================
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "stackedit.fullname" . }}
  labels:
{{ include "stackedit.labels" . | indent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ include "stackedit.name" . }}
      app.kubernetes.io/instance: {{ .Release.Name }}
  template:
    metadata:
      labels:
        app.kubernetes.io/name: {{ include "stackedit.name" . }}
        app.kubernetes.io/instance: {{ .Release.Name }}
    spec:
    {{- with .Values.imagePullSecrets }}
      imagePullSecrets:
        {{- toYaml . | nindent 8 }}
    {{- end }}
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          volumeMounts:
            - mountPath: /run
              name: run-volume
            - mountPath: /tmp
              name: tmp-volume
          env:
            - name: PORT
              value: "80"
            - name: PAYPAL_RECEIVER_EMAIL
              value: {{ .Values.paypalReceiverEmail }}
            - name: AWS_ACCESS_KEY_ID
              value: {{ .Values.awsAccessKeyId }}
            - name: AWS_SECRET_ACCESS_KEY
              value: {{ .Values.awsSecretAccessKey }}
            - name: DROPBOX_APP_KEY
              value: {{ .Values.dropboxAppKey }}
            - name: DROPBOX_APP_KEY_FULL
              value: {{ .Values.dropboxAppKeyFull }}
            - name: GOOGLE_CLIENT_ID
              value: {{ .Values.googleClientId }}
            - name: GOOGLE_API_KEY
              value: {{ .Values.googleApiKey }}
            - name: GITHUB_CLIENT_ID
              value: {{ .Values.githubClientId }}
            - name: GITHUB_CLIENT_SECRET
              value: {{ .Values.githubClientSecret }}
            - name: WORDPRESS_CLIENT_ID
              value: {{ .Values.wordpressClientId }}
            - name: WORDPRESS_SECRET
              value: {{ .Values.wordpressSecret }}
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /
              port: http
          readinessProbe:
            httpGet:
              path: /
              port: http
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
      volumes:
      - name: run-volume
        emptyDir: {}
      - name: tmp-volume
        emptyDir: {}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
    {{- with .Values.affinity }}
      affinity:
        {{- toYaml . | nindent 8 }}
    {{- end }}
    {{- with .Values.tolerations }}
      tolerations:
        {{- toYaml . | nindent 8 }}
    {{- end }}


================================================
FILE: chart/templates/ingress.yaml
================================================
{{- if .Values.ingress.enabled -}}
{{- $fullName := include "stackedit.fullname" . -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ $fullName }}
  labels:
{{ include "stackedit.labels" . | indent 4 }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
{{- if .Values.ingress.tls }}
  tls:
  {{- range .Values.ingress.tls }}
    - hosts:
      {{- range .hosts }}
        - {{ . | quote }}
      {{- end }}
      secretName: {{ .secretName }}
  {{- end }}
{{- end }}
  rules:
  {{- range .Values.ingress.hosts }}
    - host: {{ .host | quote }}
      http:
        paths:
        {{- range .paths }}
          - path: {{ . }}
            pathType: Prefix
            backend:
              service:
                name: {{ $fullName }}
                port:
                  name: http
        {{- end }}
  {{- end }}
{{- end }}


================================================
FILE: chart/templates/service.yaml
================================================
apiVersion: v1
kind: Service
metadata:
  name: {{ include "stackedit.fullname" . }}
  labels:
{{ include "stackedit.labels" . | indent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
    - port: {{ .Values.service.port }}
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app.kubernetes.io/name: {{ include "stackedit.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}


================================================
FILE: chart/templates/tests/test-connection.yaml
================================================
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "stackedit.fullname" . }}-test-connection"
  labels:
{{ include "stackedit.labels" . | indent 4 }}
  annotations:
    "helm.sh/hook": test-success
spec:
  containers:
    - name: wget
      image: busybox
      command: ['wget']
      args:  ['{{ include "stackedit.fullname" . }}:{{ .Values.service.port }}']
  restartPolicy: Never


================================================
FILE: chart/values.yaml
================================================
# Default values for stackedit.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

dropboxAppKey: ""
dropboxAppKeyFull: ""
googleClientId: ""
googleApiKey: ""
githubClientId: ""
githubClientSecret: ""
wordpressClientId: ""
wordpressSecret: ""
paypalReceiverEmail: ""
awsAccessKeyId: ""
awsSecretAccessKey: ""

replicaCount: 1

image:
  repository: benweet/stackedit
  tag: vSTACKEDIT_VERSION
  pullPolicy: IfNotPresent

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: false
  annotations:
   # kubernetes.io/ingress.class: nginx
   # certmanager.k8s.io/issuer: letsencrypt-prod
   # certmanager.k8s.io/acme-challenge-type: http01
  hosts: []
   # - host: stackedit.example.com
   #   paths:
   #     - /

  tls: []
   # - secretName: stackedit-tls
   #   hosts:
   #     - stackedit.example.com

resources: {}
  # We usually recommend not to specify default resources and to leave this as a conscious
  # choice for the user. This also increases chances charts run on environments with little
  # resources, such as Minikube. If you do want to specify resources, uncomment the following
  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
  # limits:
  #   cpu: 100m
  #   memory: 128Mi
  # requests:
  #   cpu: 100m
  #   memory: 128Mi

nodeSelector: {}

tolerations: []

affinity: {}


================================================
FILE: chrome-app/manifest.json
================================================
{
  "name": "StackEdit",
  "description": "In-browser Markdown editor",
  "version": "1.0.13",
  "manifest_version": 2,
  "container" : "GOOGLE_DRIVE",
  "api_console_project_id" : "241271498917",
  "icons": {
    "16": "icon-16.png",
    "32": "icon-32.png",
    "64": "icon-64.png",
    "128": "icon-128.png",
    "256": "icon-256.png",
    "512": "icon-512.png"
  },
  "app": {
    "urls": [
      "https://stackedit.io/"
    ],
    "launch": {
      "web_url": "https://stackedit.io/app"
    }
  },
  "offline_enabled": true,
  "permissions": [
    "unlimitedStorage"
  ]
}


================================================
FILE: config/dev.env.js
================================================
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"'
})


================================================
FILE: config/index.js
================================================
// see http://vuejs-templates.github.io/webpack for documentation.
var path = require('path')

module.exports = {
  build: {
    env: require('./prod.env'),
    index: path.resolve(__dirname, '../dist/index.html'),
    assetsRoot: path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    productionSourceMap: true,
    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css'],
    // Run the build command with an extra argument to
    // View the bundle analyzer report after build finishes:
    // `npm run build --report`
    // Set to `true` or `false` to always turn it on or off
    bundleAnalyzerReport: process.env.npm_config_report
  },
  dev: {
    env: require('./dev.env'),
    port: 8080,
    autoOpenBrowser: false,
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {},
    // CSS Sourcemaps off by default because relative paths are "buggy"
    // with this option, according to the CSS-Loader README
    // (https://github.com/webpack/css-loader#sourcemaps)
    // In our experience, they generally work as expected,
    // just be aware of this issue when enabling this option.
    // cssSourceMap: false
    cssSourceMap: true
  }
}


================================================
FILE: config/prod.env.js
================================================
module.exports = {
  NODE_ENV: '"production"'
}


================================================
FILE: gulpfile.js
================================================
const path = require('path');
const gulp = require('gulp');
const concat = require('gulp-concat');

const prismScripts = [
  'prismjs/components/prism-core',
  'prismjs/components/prism-markup',
  'prismjs/components/prism-clike',
  'prismjs/components/prism-c',
  'prismjs/components/prism-javascript',
  'prismjs/components/prism-css',
  'prismjs/components/prism-ruby',
  'prismjs/components/prism-cpp',
].map(require.resolve);
prismScripts.push(
  path.join(path.dirname(require.resolve('prismjs/components/prism-core')), 'prism-!(*.min).js'));

gulp.task('build-prism', () => gulp.src(prismScripts)
  .pipe(concat('prism.js'))
  .pipe(gulp.dest(path.dirname(require.resolve('prismjs')))));


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>StackEdit</title>
    <link rel="canonical" href="https://stackedit.io/app">
    <meta name="description" content="Free, open-source, full-featured Markdown editor.">
    <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>


================================================
FILE: index.js
================================================
const env = require('./config/prod.env');

Object.keys(env).forEach((key) => {
  if (!process.env[key]) {
    process.env[key] = JSON.parse(env[key]);
  }
});

const http = require('http');
const express = require('express');

const app = express();

require('./server')(app);

const port = parseInt(process.env.PORT || 8080, 10);
const httpServer = http.createServer(app);
httpServer.listen(port, null, () => {
  console.log(`HTTP server started: http://localhost:${port}`);
});

// Handle graceful shutdown
process.on('SIGTERM', () => {
  httpServer.close(() => {
    process.exit(0);
  });
});


================================================
FILE: package.json
================================================
{
  "name": "stackedit",
  "version": "5.15.4",
  "description": "Free, open-source, full-featured Markdown editor",
  "author": "Benoit Schweblin",
  "license": "Apache-2.0",
  "bugs": {
    "url": "https://github.com/benweet/stackedit/issues"
  },
  "main": "index.js",
  "scripts": {
    "postinstall": "gulp build-prism",
    "start": "node build/dev-server.js",
    "build": "node build/build.js && npm run build-style",
    "build-style": "webpack --config build/webpack.style.conf.js",
    "lint": "eslint --ext .js,.vue src server",
    "unit": "jest --config test/unit/jest.conf.js --runInBand",
    "unit-with-coverage": "jest --config test/unit/jest.conf.js --runInBand --coverage",
    "test": "npm run lint && npm run unit",
    "preversion": "npm run test",
    "postversion": "git push origin master --tags && npm publish",
    "patch": "npm version patch -m \"Tag v%s\"",
    "minor": "npm version minor -m \"Tag v%s\"",
    "major": "npm version major -m \"Tag v%s\"",
    "chart": "mkdir -p dist && rm -rf dist/stackedit && cp -r chart dist/stackedit && sed -i.bak -e s/STACKEDIT_VERSION/$npm_package_version/g dist/stackedit/*.yaml && rm dist/stackedit/*.yaml.bak"
  },
  "dependencies": {
    "@vue/test-utils": "^1.0.0-beta.16",
    "abcjs": "^5.2.0",
    "aws-sdk": "^2.1380.0",
    "babel-runtime": "^6.26.0",
    "bezier-easing": "^1.1.0",
    "body-parser": "^1.18.2",
    "clipboard": "^1.7.1",
    "compression": "^1.7.0",
    "diff-match-patch": "^1.0.0",
    "file-saver": "^1.3.8",
    "google-id-token-verifier": "^0.2.3",
    "handlebars": "^4.0.10",
    "indexeddbshim": "^3.6.2",
    "js-yaml": "^3.11.0",
    "katex": "^0.13.0",
    "markdown-it": "^8.4.1",
    "markdown-it-abbr": "^1.0.4",
    "markdown-it-deflist": "^2.0.2",
    "markdown-it-emoji": "^1.3.0",
    "markdown-it-footnote": "^3.0.1",
    "markdown-it-imsize": "^2.0.1",
    "markdown-it-mark": "^2.0.0",
    "markdown-it-pandoc-renderer": "1.2.0",
    "markdown-it-sub": "^1.0.0",
    "markdown-it-sup": "^1.0.0",
    "mermaid": "^8.9.2",
    "mousetrap": "^1.6.1",
    "normalize-scss": "^7.0.1",
    "prismjs": "^1.6.0",
    "request": "^2.85.0",
    "serve-static": "^1.13.2",
    "tmp": "^0.0.33",
    "turndown": "^4.0.2",
    "vue": "^2.5.16",
    "vuex": "^3.0.1"
  },
  "devDependencies": {
    "autoprefixer": "^6.7.2",
    "babel-core": "^6.26.3",
    "babel-eslint": "^8.2.3",
    "babel-jest": "^21.0.2",
    "babel-loader": "^7.1.4",
    "babel-plugin-dynamic-import-node": "^1.2.0",
    "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
    "babel-plugin-transform-runtime": "^6.23.0",
    "babel-polyfill": "^6.23.0",
    "babel-preset-env": "^1.7.0",
    "babel-preset-stage-2": "^6.22.0",
    "babel-register": "^6.22.0",
    "chalk": "^1.1.3",
    "connect-history-api-fallback": "^1.3.0",
    "copy-webpack-plugin": "^4.5.1",
    "css-loader": "^0.28.11",
    "eslint": "^4.19.1",
    "eslint-config-airbnb-base": "^12.1.0",
    "eslint-friendly-formatter": "^4.0.1",
    "eslint-import-resolver-webpack": "^0.9.0",
    "eslint-loader": "^2.0.0",
    "eslint-plugin-html": "^4.0.3",
    "eslint-plugin-import": "^2.11.0",
    "eventsource-polyfill": "^0.9.6",
    "express": "^4.16.3",
    "extract-text-webpack-plugin": "^2.0.0",
    "favicons-webpack-plugin": "^0.0.9",
    "file-loader": "^1.1.11",
    "friendly-errors-webpack-plugin": "^1.7.0",
    "gulp": "^4.0.2",
    "gulp-concat": "^2.6.1",
    "html-webpack-plugin": "^3.2.0",
    "http-proxy-middleware": "^0.18.0",
    "identity-obj-proxy": "^3.0.0",
    "ignore-loader": "^0.1.2",
    "jest": "^23.0.0",
    "jest-raw-loader": "^1.0.1",
    "jest-serializer-vue": "^0.3.0",
    "node-sass": "^4.0.0",
    "npm-bump": "^0.0.23",
    "offline-plugin": "^5.0.3",
    "opn": "^4.0.2",
    "optimize-css-assets-webpack-plugin": "^1.3.2",
    "ora": "^1.2.0",
    "raw-loader": "^0.5.1",
    "replace-in-file": "^4.1.0",
    "rimraf": "^2.6.0",
    "sass-loader": "^7.0.1",
    "semver": "^5.5.0",
    "shelljs": "^0.8.1",
    "string-replace-loader": "^2.1.1",
    "stylelint": "^9.2.0",
    "stylelint-config-standard": "^16.0.0",
    "stylelint-processor-html": "^1.0.0",
    "stylelint-webpack-plugin": "^0.10.4",
    "url-loader": "^1.0.1",
    "vue-jest": "^1.0.2",
    "vue-loader": "^15.0.9",
    "vue-style-loader": "^4.1.0",
    "vue-template-compiler": "^2.5.16",
    "webpack": "^2.6.1",
    "webpack-bundle-analyzer": "^3.3.2",
    "webpack-dev-middleware": "^1.10.0",
    "webpack-hot-middleware": "^2.18.0",
    "webpack-merge": "^4.1.2",
    "webpack-pwa-manifest": "^3.7.1",
    "worker-loader": "^1.1.1"
  },
  "engines": {
    "node": ">= 8.0.0",
    "npm": ">= 5.0.0"
  },
  "browserslist": [
    "> 1%",
    "last 2 versions",
    "not ie <= 10"
  ]
}


================================================
FILE: server/conf.js
================================================
const pandocPath = process.env.PANDOC_PATH || 'pandoc';
const wkhtmltopdfPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
const userBucketName = process.env.USER_BUCKET_NAME || 'stackedit-users';
const paypalUri = process.env.PAYPAL_URI || 'https://www.paypal.com/cgi-bin/webscr';
const paypalReceiverEmail = process.env.PAYPAL_RECEIVER_EMAIL;

const dropboxAppKey = process.env.DROPBOX_APP_KEY;
const dropboxAppKeyFull = process.env.DROPBOX_APP_KEY_FULL;
const githubClientId = process.env.GITHUB_CLIENT_ID;
const githubClientSecret = process.env.GITHUB_CLIENT_SECRET;
const googleClientId = process.env.GOOGLE_CLIENT_ID;
const googleApiKey = process.env.GOOGLE_API_KEY;
const wordpressClientId = process.env.WORDPRESS_CLIENT_ID;

exports.values = {
  pandocPath,
  wkhtmltopdfPath,
  userBucketName,
  paypalUri,
  paypalReceiverEmail,
  dropboxAppKey,
  dropboxAppKeyFull,
  githubClientId,
  githubClientSecret,
  googleClientId,
  googleApiKey,
  wordpressClientId,
};

exports.publicValues = {
  dropboxAppKey,
  dropboxAppKeyFull,
  githubClientId,
  googleClientId,
  googleApiKey,
  wordpressClientId,
  allowSponsorship: !!paypalReceiverEmail,
};


================================================
FILE: server/github.js
================================================
const qs = require('qs'); // eslint-disable-line import/no-extraneous-dependencies
const request = require('request');
const conf = require('./conf');

function githubToken(clientId, code) {
  return new Promise((resolve, reject) => {
    request({
      method: 'POST',
      url: 'https://github.com/login/oauth/access_token',
      qs: {
        client_id: clientId,
        client_secret: conf.values.githubClientSecret,
        code,
      },
    }, (err, res, body) => {
      if (err) {
        reject(err);
      }
      const token = qs.parse(body).access_token;
      if (token) {
        resolve(token);
      } else {
        reject(res.statusCode);
      }
    });
  });
}

exports.githubToken = (req, res) => {
  githubToken(req.query.clientId, req.query.code)
    .then(
      token => res.send(token),
      err => res
        .status(400)
        .send(err ? err.message || err.toString() : 'bad_code'),
    );
};


================================================
FILE: server/index.js
================================================
const compression = require('compression');
const serveStatic = require('serve-static');
const bodyParser = require('body-parser');
const path = require('path');
const user = require('./user');
const github = require('./github');
const pdf = require('./pdf');
const pandoc = require('./pandoc');
const conf = require('./conf');

const resolvePath = pathToResolve => path.join(__dirname, '..', pathToResolve);

module.exports = (app) => {
  if (process.env.NODE_ENV === 'production') {
    // Enable CORS for fonts
    app.all('*', (req, res, next) => {
      if (/\.(eot|ttf|woff2?|svg)$/.test(req.url)) {
        res.header('Access-Control-Allow-Origin', '*');
      }
      next();
    });

    // Use gzip compression
    app.use(compression());
  }

  app.get('/oauth2/githubToken', github.githubToken);
  app.get('/conf', (req, res) => res.send(conf.publicValues));
  app.get('/userInfo', user.userInfo);
  app.post('/pdfExport', pdf.generate);
  app.post('/pandocExport', pandoc.generate);
  app.post('/paypalIpn', bodyParser.urlencoded({
    extended: false,
  }), user.paypalIpn);

  // Serve landing.html
  app.get('/', (req, res) => res.sendFile(resolvePath('static/landing/index.html')));
  // Serve sitemap.xml
  app.get('/sitemap.xml', (req, res) => res.sendFile(resolvePath('static/sitemap.xml')));
  // Serve callback.html
  app.get('/oauth2/callback', (req, res) => res.sendFile(resolvePath('static/oauth2/callback.html')));
  // Google Drive action receiver
  app.get('/googleDriveAction', (req, res) =>
    res.redirect(`./app#providerId=googleDrive&state=${encodeURIComponent(req.query.state)}`));

  // Serve static resources
  if (process.env.NODE_ENV === 'production') {
    // Serve index.html in /app
    app.get('/app', (req, res) => res.sendFile(resolvePath('dist/index.html')));

    // Serve style.css with 1 day max-age
    app.get('/style.css', (req, res) => res.sendFile(resolvePath('dist/style.css'), {
      maxAge: '1d',
    }));

    // Serve the static folder with 1 year max-age
    app.use('/static', serveStatic(resolvePath('dist/static'), {
      maxAge: '1y',
    }));

    app.use(serveStatic(resolvePath('dist')));
  }
};


================================================
FILE: server/pandoc.js
================================================
/* global window */
const { spawn } = require('child_process');
const fs = require('fs');
const tmp = require('tmp');
const user = require('./user');
const conf = require('./conf');

const outputFormats = {
  asciidoc: 'text/plain',
  context: 'application/x-latex',
  epub: 'application/epub+zip',
  epub3: 'application/epub+zip',
  latex: 'application/x-latex',
  odt: 'application/vnd.oasis.opendocument.text',
  pdf: 'application/pdf',
  rst: 'text/plain',
  rtf: 'application/rtf',
  textile: 'text/plain',
  docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
};

const highlightStyles = [
  'pygments',
  'kate',
  'monochrome',
  'espresso',
  'zenburn',
  'haddock',
  'tango',
];

const readJson = (str) => {
  try {
    return JSON.parse(str);
  } catch (e) {
    return {};
  }
};

exports.generate = (req, res) => {
  let pandocError = '';
  const outputFormat = Object.prototype.hasOwnProperty.call(outputFormats, req.query.format)
    ? req.query.format
    : 'pdf';
  user.checkSponsor(req.query.idToken)
    .then((isSponsor) => {
      if (!isSponsor) {
        throw new Error('unauthorized');
      }

      return new Promise((resolve, reject) => {
        tmp.file({
          postfix: `.${outputFormat}`,
        }, (err, filePath, fd, cleanupCallback) => {
          if (err) {
            reject(err);
          } else {
            resolve({
              filePath,
              cleanupCallback,
            });
          }
        });
      });
    })
    .then(({ filePath, cleanupCallback }) => new Promise((resolve, reject) => {
      const options = readJson(req.query.options);
      const metadata = readJson(req.query.metadata);
      const params = [];

      params.push('--pdf-engine=xelatex');
      params.push('--webtex=http://chart.apis.google.com/chart?cht=tx&chf=bg,s,FFFFFF00&chco=000000&chl=');
      if (options.toc) {
        params.push('--toc');
      }
      options.tocDepth = parseInt(options.tocDepth, 10);
      if (!Number.isNaN(options.tocDepth)) {
        params.push('--toc-depth', options.tocDepth);
      }
      options.highlightStyle = highlightStyles.includes(options.highlightStyle) ? options.highlightStyle : 'kate';
      params.push('--highlight-style', options.highlightStyle);
      Object.keys(metadata).forEach((key) => {
        params.push('-M', `${key}=${metadata[key]}`);
      });

      let finished = false;

      function onError(error) {
        finished = true;
        cleanupCallback();
        reject(error);
      }

      const format = outputFormat === 'pdf' ? 'latex' : outputFormat;
      params.push('-f', 'json', '-t', format, '-o', filePath);
      const pandoc = spawn(conf.values.pandocPath, params, {
        stdio: [
          'pipe',
          'ignore',
          'pipe',
        ],
      });
      let timeoutId = setTimeout(() => {
        timeoutId = null;
        pandoc.kill();
      }, 50000);
      pandoc.on('error', onError);
      pandoc.stdin.on('error', onError);
      pandoc.stderr.on('data', (data) => {
        pandocError += `${data}`;
      });
      pandoc.on('close', (code) => {
        if (!finished) {
          clearTimeout(timeoutId);
          if (!timeoutId) {
            res.statusCode = 408;
            cleanupCallback();
            reject(new Error('timeout'));
          } else if (code) {
            cleanupCallback();
            reject();
          } else {
            res.set('Content-Type', outputFormats[outputFormat]);
            const readStream = fs.createReadStream(filePath);
            readStream.on('open', () => readStream.pipe(res));
            readStream.on('close', () => cleanupCallback());
            readStream.on('error', () => {
              cleanupCallback();
              reject();
            });
          }
        }
      });
      req.pipe(pandoc.stdin);
    }))
    .catch((err) => {
      const message = err && err.message;
      if (message === 'unauthorized') {
        res.statusCode = 401;
        res.end('Unauthorized.');
      } else if (message === 'timeout') {
        res.statusCode = 408;
        res.end('Request timeout.');
      } else {
        res.statusCode = 400;
        res.end(pandocError || 'Unknown error.');
      }
    });
};


================================================
FILE: server/pdf.js
================================================
/* global window,MathJax */
const { spawn } = require('child_process');
const fs = require('fs');
const tmp = require('tmp');
const user = require('./user');
const conf = require('./conf');

/* eslint-disable no-var, prefer-arrow-callback, func-names */
function waitForJavaScript() {
  if (window.MathJax) {
    // Amazon EC2: fix TeX font detection
    MathJax.Hub.Register.StartupHook('HTML-CSS Jax Startup', function () {
      var htmlCss = MathJax.OutputJax['HTML-CSS'];
      htmlCss.Font.checkWebFont = function (check, font, callback) {
        if (check.time(callback)) {
          return;
        }
        if (check.total === 0) {
          htmlCss.Font.testFont(font);
          setTimeout(check, 200);
        } else {
          callback(check.STATUS.OK);
        }
      };
    });
    MathJax.Hub.Queue(function () {
      window.status = 'done';
    });
  } else {
    setTimeout(function () {
      window.status = 'done';
    }, 2000);
  }
}
/* eslint-disable no-var, prefer-arrow-callback, func-names */

const authorizedPageSizes = [
  'A3',
  'A4',
  'Legal',
  'Letter',
];

const readJson = (str) => {
  try {
    return JSON.parse(str);
  } catch (e) {
    return {};
  }
};

exports.generate = (req, res) => {
  let wkhtmltopdfError = '';
  user.checkSponsor(req.query.idToken)
    .then((isSponsor) => {
      if (!isSponsor) {
        throw new Error('unauthorized');
      }
      return new Promise((resolve, reject) => {
        tmp.file((err, filePath, fd, cleanupCallback) => {
          if (err) {
            reject(err);
          } else {
            resolve({
              filePath,
              cleanupCallback,
            });
          }
        });
      });
    })
    .then(({ filePath, cleanupCallback }) => new Promise((resolve, reject) => {
      let finished = false;

      function onError(err) {
        finished = true;
        cleanupCallback();
        reject(err);
      }
      const options = readJson(req.query.options);
      const params = [];

      // Margins
      const marginTop = parseInt(`${options.marginTop}`, 10);
      params.push('-T', Number.isNaN(marginTop) ? 25 : marginTop);
      const marginRight = parseInt(`${options.marginRight}`, 10);
      params.push('-R', Number.isNaN(marginRight) ? 25 : marginRight);
      const marginBottom = parseInt(`${options.marginBottom}`, 10);
      params.push('-B', Number.isNaN(marginBottom) ? 25 : marginBottom);
      const marginLeft = parseInt(`${options.marginLeft}`, 10);
      params.push('-L', Number.isNaN(marginLeft) ? 25 : marginLeft);

      // Header
      if (options.headerCenter) {
        params.push('--header-center', `${options.headerCenter}`);
      }
      if (options.headerLeft) {
        params.push('--header-left', `${options.headerLeft}`);
      }
      if (options.headerRight) {
        params.push('--header-right', `${options.headerRight}`);
      }
      if (options.headerFontName) {
        params.push('--header-font-name', `${options.headerFontName}`);
      }
      if (options.headerFontSize) {
        params.push('--header-font-size', `${options.headerFontSize}`);
      }

      // Footer
      if (options.footerCenter) {
        params.push('--footer-center', `${options.footerCenter}`);
      }
      if (options.footerLeft) {
        params.push('--footer-left', `${options.footerLeft}`);
      }
      if (options.footerRight) {
        params.push('--footer-right', `${options.footerRight}`);
      }
      if (options.footerFontName) {
        params.push('--footer-font-name', `${options.footerFontName}`);
      }
      if (options.footerFontSize) {
        params.push('--footer-font-size', `${options.footerFontSize}`);
      }

      // Page size
      params.push('--page-size', !authorizedPageSizes.includes(options.pageSize) ? 'A4' : options.pageSize);

      // Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason
      params.push('--run-script', `${waitForJavaScript.toString()}waitForJavaScript()`);
      params.push('--window-status', 'done');
      const wkhtmltopdf = spawn(conf.values.wkhtmltopdfPath, params.concat('-', filePath), {
        stdio: [
          'pipe',
          'ignore',
          'pipe',
        ],
      });
      let timeoutId = setTimeout(function () {
        timeoutId = null;
        wkhtmltopdf.kill();
      }, 50000);
      wkhtmltopdf.on('error', onError);
      wkhtmltopdf.stdin.on('error', onError);
      wkhtmltopdf.stderr.on('data', (data) => {
        wkhtmltopdfError += `${data}`;
      });
      wkhtmltopdf.on('close', (code) => {
        if (!finished) {
          clearTimeout(timeoutId);
          if (!timeoutId) {
            cleanupCallback();
            reject(new Error('timeout'));
          } else if (code) {
            cleanupCallback();
            reject();
          } else {
            res.set('Content-Type', 'application/pdf');
            const readStream = fs.createReadStream(filePath);
            readStream.on('open', () => readStream.pipe(res));
            readStream.on('close', () => cleanupCallback());
            readStream.on('error', () => {
              cleanupCallback();
              reject();
            });
          }
        }
      });
      req.pipe(wkhtmltopdf.stdin);
    }))
    .catch((err) => {
      const message = err && err.message;
      if (message === 'unauthorized') {
        res.statusCode = 401;
        res.end('Unauthorized.');
      } else if (message === 'timeout') {
        res.statusCode = 408;
        res.end('Request timeout.');
      } else {
        res.statusCode = 400;
        res.end(wkhtmltopdfError || 'Unknown error.');
      }
    });
};


================================================
FILE: server/user.js
================================================
const request = require('request');
const AWS = require('aws-sdk');
const verifier = require('google-id-token-verifier');
const conf = require('./conf');

const s3Client = new AWS.S3();

const cb = (resolve, reject) => (err, res) => {
  if (err) {
    reject(err);
  } else {
    resolve(res);
  }
};

exports.getUser = id => new Promise((resolve, reject) => {
  s3Client.getObject({
    Bucket: conf.values.userBucketName,
    Key: id,
  }, cb(resolve, reject));
})
  .then(
    res => JSON.parse(res.Body.toString('utf-8')),
    (err) => {
      if (err.code !== 'NoSuchKey') {
        throw err;
      }
    },
  );

exports.putUser = (id, user) => new Promise((resolve, reject) => {
  s3Client.putObject({
    Bucket: conf.values.userBucketName,
    Key: id,
    Body: JSON.stringify(user),
  }, cb(resolve, reject));
});

exports.getUserFromToken = idToken => new Promise((resolve, reject) => verifier
  .verify(idToken, conf.values.googleClientId, cb(resolve, reject)))
  .then(tokenInfo => exports.getUser(tokenInfo.sub));

exports.userInfo = (req, res) => exports.getUserFromToken(req.query.idToken)
  .then(
    user => res.send(Object.assign({
      sponsorUntil: 0,
    }, user)),
    err => res
      .status(400)
      .send(err ? err.message || err.toString() : 'invalid_token'),
  );

exports.paypalIpn = (req, res, next) => Promise.resolve()
  .then(() => {
    const userId = req.body.custom;
    const paypalEmail = req.body.payer_email;
    const gross = parseFloat(req.body.mc_gross);
    let sponsorUntil;
    if (gross === 5) {
      sponsorUntil = Date.now() + (3 * 31 * 24 * 60 * 60 * 1000); // 3 months
    } else if (gross === 15) {
      sponsorUntil = Date.now() + (366 * 24 * 60 * 60 * 1000); // 1 year
    } else if (gross === 25) {
      sponsorUntil = Date.now() + (2 * 366 * 24 * 60 * 60 * 1000); // 2 years
    } else if (gross === 50) {
      sponsorUntil = Date.now() + (5 * 366 * 24 * 60 * 60 * 1000); // 5 years
    }
    if (
      req.body.receiver_email !== conf.values.paypalReceiverEmail ||
      req.body.payment_status !== 'Completed' ||
      req.body.mc_currency !== 'USD' ||
      (req.body.txn_type !== 'web_accept' && req.body.txn_type !== 'subscr_payment') ||
      !userId || !sponsorUntil
    ) {
      // Ignoring PayPal IPN
      return res.end();
    }
    // Processing PayPal IPN
    req.body.cmd = '_notify-validate';
    return new Promise((resolve, reject) => request.post({
      uri: conf.values.paypalUri,
      form: req.body,
    }, (err, response, body) => {
      if (err) {
        reject(err);
      } else if (body !== 'VERIFIED') {
        reject(new Error('PayPal IPN unverified'));
      } else {
        resolve();
      }
    }))
      .then(() => exports.putUser(userId, {
        paypalEmail,
        sponsorUntil,
      }))
      .then(() => res.end());
  })
  .catch(next);

exports.checkSponsor = (idToken) => {
  if (!conf.publicValues.allowSponsorship) {
    return Promise.resolve(true);
  }
  if (!idToken) {
    return Promise.resolve(false);
  }
  return exports.getUserFromToken(idToken)
    .then(userInfo => userInfo && userInfo.sponsorUntil > Date.now(), () => false);
};


================================================
FILE: src/components/App.vue
================================================
<template>
  <div class="app" :class="classes" @keydown.esc="close">
    <splash-screen v-if="!ready"></splash-screen>
    <layout v-else></layout>
    <modal></modal>
    <notification></notification>
    <context-menu></context-menu>
  </div>
</template>

<script>
import '../styles';
import '../styles/markdownHighlighting.scss';
import '../styles/app.scss';
import Layout from './Layout';
import Modal from './Modal';
import Notification from './Notification';
import ContextMenu from './ContextMenu';
import SplashScreen from './SplashScreen';
import syncSvc from '../services/syncSvc';
import networkSvc from '../services/networkSvc';
import tempFileSvc from '../services/tempFileSvc';
import store from '../store';
import './common/vueGlobals';

const themeClasses = {
  light: ['app--light'],
  dark: ['app--dark'],
};

export default {
  components: {
    Layout,
    Modal,
    Notification,
    ContextMenu,
    SplashScreen,
  },
  data: () => ({
    ready: false,
  }),
  computed: {
    classes() {
      const result = themeClasses[store.getters['data/computedSettings'].colorTheme];
      return Array.isArray(result) ? result : themeClasses.light;
    },
  },
  methods: {
    close() {
      tempFileSvc.close();
    },
  },
  async created() {
    try {
      await syncSvc.init();
      await networkSvc.init();
      this.ready = true;
      tempFileSvc.setReady();
    } catch (err) {
      if (err && err.message === 'RELOAD') {
        window.location.reload();
      } else if (err && err.message !== 'RELOAD') {
        console.error(err); // eslint-disable-line no-console
        store.dispatch('notification/error', err);
      }
    }
  },
};
</script>


================================================
FILE: src/components/ButtonBar.vue
================================================
<template>
  <div class="button-bar">
    <div class="button-bar__inner button-bar__inner--top">
      <button class="button-bar__button button-bar__button--navigation-bar-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showNavigationBar }" v-if="!light" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
        <icon-navigation-bar></icon-navigation-bar>
      </button>
      <button class="button-bar__button button-bar__button--side-preview-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showSidePreview }" tour-step-anchor="editor" @click="toggleSidePreview()" v-title="'Toggle side preview'">
        <icon-side-preview></icon-side-preview>
      </button>
      <button class="button-bar__button button-bar__button--editor-toggler button" @click="toggleEditor(false)" v-title="'Reader mode'">
        <icon-eye></icon-eye>
      </button>
    </div>
    <div class="button-bar__inner button-bar__inner--bottom">
      <button class="button-bar__button button-bar__button--focus-mode-toggler button" :class="{ 'button-bar__button--on': layoutSettings.focusMode }" @click="toggleFocusMode()" v-title="'Toggle focus mode'">
        <icon-target></icon-target>
      </button>
      <button class="button-bar__button button-bar__button--scroll-sync-toggler button" :class="{ 'button-bar__button--on': layoutSettings.scrollSync }" @click="toggleScrollSync()" v-title="'Toggle scroll sync'">
        <icon-scroll-sync></icon-scroll-sync>
      </button>
      <button class="button-bar__button button-bar__button--status-bar-toggler button" :class="{ 'button-bar__button--on': layoutSettings.showStatusBar }" @click="toggleStatusBar()" v-title="'Toggle status bar'">
        <icon-status-bar></icon-status-bar>
      </button>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from 'vuex';

export default {
  computed: {
    ...mapState([
      'light',
    ]),
    ...mapGetters('data', [
      'layoutSettings',
    ]),
  },
  methods: mapActions('data', [
    'toggleNavigationBar',
    'toggleEditor',
    'toggleSidePreview',
    'toggleStatusBar',
    'toggleFocusMode',
    'toggleScrollSync',
  ]),
};
</script>

<style lang="scss">
@import '../styles/variables.scss';

.button-bar {
  position: absolute;
  width: 100%;
  height: 100%;
}

.button-bar__inner {
  position: absolute;
}

.button-bar__inner--bottom {
  bottom: 0;
}

.button-bar__button {
  color: rgba(0, 0, 0, 0.2);
  display: block;
  width: 26px;
  height: 26px;
  padding: 2px;
  margin: 3px 0;

  .app--dark & {
    color: rgba(255, 255, 255, 0.15);
  }

  &:active,
  &:focus,
  &:hover {
    color: rgba(0, 0, 0, 0.2);

    .app--dark & {
      color: rgba(255, 255, 255, 0.15);
      background-color: $navbar-hover-background;
    }
  }
}

.button-bar__button--on {
  color: rgba(0, 0, 0, 0.4);

  .app--dark & {
    color: rgba(255, 255, 255, 0.4);
  }

  &:active,
  &:focus,
  &:hover {
    color: rgba(0, 0, 0, 0.4);

    .app--dark & {
      color: rgba(255, 255, 255, 0.4);
    }
  }
}
</style>


================================================
FILE: src/components/CodeEditor.vue
================================================
<template>
  <pre class="code-editor textfield prism" :disabled="disabled"></pre>
</template>

<script>
import Prism from 'prismjs';
import cledit from '../services/editor/cledit';

export default {
  props: ['value', 'lang', 'disabled'],
  mounted() {
    const preElt = this.$el;
    let scrollElt = preElt;
    while (scrollElt && !scrollElt.classList.contains('modal')) {
      scrollElt = scrollElt.parentNode;
    }
    if (scrollElt) {
      const clEditor = cledit(preElt, scrollElt);
      clEditor.on('contentChanged', value => this.$emit('changed', value));
      clEditor.init({
        content: this.value,
        sectionHighlighter: section => Prism.highlight(section.text, Prism.languages[this.lang]),
      });
      clEditor.toggleEditable(!this.disabled);
    }
  },
};
</script>

<style lang="scss">
@import '../styles/variables.scss';

.code-editor {
  margin: 0;
  font-family: $font-family-monospace;
  font-size: $font-size-monospace;
  font-variant-ligatures: no-common-ligatures;
  word-break: break-word;
  word-wrap: normal;
  height: auto;
  caret-color: #000;
  min-height: 160px;
  overflow: auto;
  padding: 0.2em 0.4em;

  * {
    line-height: $line-height-base;
    font-size: inherit !important;
  }
}
</style>


================================================
FILE: src/components/ContextMenu.vue
================================================
<template>
  <div class="context-menu" v-if="items.length" @click="close()" @contextmenu.prevent="close()">
    <div class="context-menu__inner flex flex--column" :style="{ left: coordinates.left + 'px', top: coordinates.top + 'px' }" @click.stop>
      <div v-for="(item, idx) in items" :key="idx">
        <div class="context-menu__separator" v-if="item.type === 'separator'"></div>
        <div class="context-menu__item context-menu__item--disabled" v-else-if="item.disabled">{{item.name}}</div>
        <a class="context-menu__item" href="javascript:void(0)" v-else @click="close(item)">{{item.name}}</a>
      </div>
    </div>
  </div>
</template>

<script>
import { mapState } from 'vuex';
import store from '../store';

export default {
  computed: {
    ...mapState('contextMenu', [
      'coordinates',
      'items',
      'resolve',
    ]),
  },
  methods: {
    close(item = null) {
      this.resolve(item);
      store.dispatch('contextMenu/close');
    },
  },
};
</script>

<style lang="scss">
.context-menu {
  position: absolute;
  width: 100%;
  height: 100%;
  font-size: 14px;
  line-height: 18px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  user-select: none;
}

$padding: 5px;

.context-menu__inner {
  position: absolute;
  background-color: #ebebeb;
  border-radius: $padding;
  padding: $padding 0;
  box-shadow: 0 6px 10px rgba(0, 0, 0, 0.16), 0 3px 10px 1px rgba(0, 0, 0, 0.12);
}

.context-menu__item {
  display: block;
  color: #333;
  text-decoration: none;
  padding: 0 25px;
}

a.context-menu__item {
  &:active,
  &:focus,
  &:hover {
    background-color: #338dfc;
    color: #fff;
  }
}

.context-menu__item--disabled {
  color: #aaa;
}

.context-menu__separator {
  border-top: 2px solid #dcdcdd;
  margin: $padding 0;
}
</style>


================================================
FILE: src/components/Editor.vue
================================================
<template>
  <div class="editor">
    <pre class="editor__inner markdown-highlighting" :style="{padding: styles.editorPadding}" :class="{monospaced: computedSettings.editor.monospacedFontOnly}"></pre>
    <div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
      <comment-list v-if="styles.editorGutterWidth"></comment-list>
      <editor-new-discussion-button v-if="!isCurrentTemp"></editor-new-discussion-button>
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex';
import CommentList from './gutters/CommentList';
import EditorNewDiscussionButton from './gutters/EditorNewDiscussionButton';
import store from '../store';

export default {
  components: {
    CommentList,
    EditorNewDiscussionButton,
  },
  computed: {
    ...mapGetters('file', [
      'isCurrentTemp',
    ]),
    ...mapGetters('layout', [
      'styles',
    ]),
    ...mapGetters('data', [
      'computedSettings',
    ]),
  },
  mounted() {
    const editorElt = this.$el.querySelector('.editor__inner');
    const onDiscussionEvt = cb => (evt) => {
      let elt = evt.target;
      while (elt && elt !== editorElt) {
        if (elt.discussionId) {
          cb(elt.discussionId);
          return;
        }
        elt = elt.parentNode;
      }
    };

    const classToggler = toggle => (discussionId) => {
      editorElt.getElementsByClassName(`discussion-editor-highlighting--${discussionId}`)
        .cl_each(elt => elt.classList.toggle('discussion-editor-highlighting--hover', toggle));
      document.getElementsByClassName(`comment--discussion-${discussionId}`)
        .cl_each(elt => elt.classList.toggle('comment--hover', toggle));
    };

    editorElt.addEventListener('mouseover', onDiscussionEvt(classToggler(true)));
    editorElt.addEventListener('mouseout', onDiscussionEvt(classToggler(false)));
    editorElt.addEventListener('click', onDiscussionEvt((discussionId) => {
      store.commit('discussion/setCurrentDiscussionId', discussionId);
    }));

    this.$watch(
      () => store.state.discussion.currentDiscussionId,
      (discussionId, oldDiscussionId) => {
        if (oldDiscussionId) {
          editorElt.querySelectorAll(`.discussion-editor-highlighting--${oldDiscussionId}`)
            .cl_each(elt => elt.classList.remove('discussion-editor-highlighting--selected'));
        }
        if (discussionId) {
          editorElt.querySelectorAll(`.discussion-editor-highlighting--${discussionId}`)
            .cl_each(elt => elt.classList.add('discussion-editor-highlighting--selected'));
        }
      },
    );
  },
};
</script>

<style lang="scss">
@import '../styles/variables.scss';

.editor {
  position: absolute;
  width: 100%;
  height: 100%;
  overflow: auto;
}

.editor__inner {
  margin: 0;
  font-family: $font-family-main;
  font-variant-ligatures: no-common-ligatures;
  white-space: pre-wrap;
  word-break: break-word;
  word-wrap: break-word;

  * {
    line-height: $line-height-base;
  }

  .cledit-section {
    font-family: inherit;
  }

  .hide {
    display: none;
  }

  &.monospaced {
    font-family: $font-family-monospace !important;
    font-size: $font-size-monospace !important;

    * {
      font-size: inherit !important;
    }
  }
}
</style>


================================================
FILE: src/components/Explorer.vue
================================================
<template>
  <div class="explorer flex flex--column">
    <div class="side-title flex flex--row flex--space-between">
      <div class="flex flex--row">
        <button class="side-title__button side-title__button--new-file button" @click="newItem()" v-title="'New file'">
          <icon-file-plus></icon-file-plus>
        </button>
        <button class="side-title__button side-title__button--new-folder button" @click="newItem(true)" v-title="'New folder'">
          <icon-folder-plus></icon-folder-plus>
        </button>
        <button class="side-title__button side-title__button--delete button" @click="deleteItem()" v-title="'Delete'">
          <icon-delete></icon-delete>
        </button>
        <button class="side-title__button side-title__button--rename button" @click="editItem()" v-title="'Rename'">
          <icon-pen></icon-pen>
        </button>
      </div>
      <button class="side-title__button side-title__button--close button" @click="toggleExplorer(false)" v-title="'Close explorer'">
        <icon-close></icon-close>
      </button>
    </div>
    <div class="explorer__tree" :class="{'explorer__tree--new-item': !newChildNode.isNil}" v-if="!light" tabindex="0" @keydown.delete="deleteItem()">
      <explorer-node :node="rootNode" :depth="0"></explorer-node>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import ExplorerNode from './ExplorerNode';
import explorerSvc from '../services/explorerSvc';
import store from '../store';

export default {
  components: {
    ExplorerNode,
  },
  computed: {
    ...mapState([
      'light',
    ]),
    ...mapState('explorer', [
      'newChildNode',
    ]),
    ...mapGetters('explorer', [
      'rootNode',
      'selectedNode',
    ]),
  },
  methods: {
    ...mapActions('data', [
      'toggleExplorer',
    ]),
    newItem: isFolder => explorerSvc.newItem(isFolder),
    deleteItem: () => explorerSvc.deleteItem(),
    editItem() {
      const node = this.selectedNode;
      if (!node.isTrash && !node.isTemp) {
        store.commit('explorer/setEditingId', node.item.id);
      }
    },
  },
  created() {
    this.$watch(
      () => store.getters['file/current'].id,
      (currentFileId) => {
        store.commit('explorer/setSelectedId', currentFileId);
        store.dispatch('explorer/openNode', currentFileId);
      }, {
        immediate: true,
      },
    );
  },
};
</script>

<style lang="scss">
.explorer,
.explorer__tree {
  height: 100%;
}

.explorer__tree {
  overflow: auto;

  /* fake element */
  & > .explorer-node > .explorer-node__children > .explorer-node:last-child > .explorer-node__item {
    height: 20px;
    cursor: auto;
  }
}
</style>


================================================
FILE: src/components/ExplorerNode.vue
================================================
<template>
  <div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--folder': node.isFolder, 'explorer-node--open': isOpen, 'explorer-node--trash': node.isTrash, 'explorer-node--temp': node.isTemp, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="node.noDrop || setDragTarget(node)" @dragleave.stop="isDragTarget && setDragTarget()" @drop.prevent.stop="onDrop" @contextmenu="onContextMenu">
    <div class="explorer-node__item-editor" v-if="isEditing" :style="{paddingLeft: leftPadding}" draggable="true" @dragstart.stop.prevent>
      <input type="text" class="text-input" v-focus @blur="submitEdit()" @keydown.stop @keydown.enter="submitEdit()" @keydown.esc.stop="submitEdit(true)" v-model="editingNodeName">
    </div>
    <div class="explorer-node__item" v-else :style="{paddingLeft: leftPadding}" @click="select()" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTarget()">
      {{node.item.name}}
      <icon-provider class="explorer-node__location" v-for="location in node.locations" :key="location.id" :provider-id="location.providerId"></icon-provider>
    </div>
    <div class="explorer-node__children" v-if="node.isFolder && isOpen">
      <explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
      <div v-if="newChild" class="explorer-node__new-child" :class="{'explorer-node__new-child--folder': newChild.isFolder}" :style="{paddingLeft: childLeftPadding}">
        <input type="text" class="text-input" v-focus @blur="submitNewChild()" @keydown.stop @keydown.enter="submitNewChild()" @keydown.esc.stop="submitNewChild(true)" v-model.trim="newChildName">
      </div>
      <explorer-node v-for="node in node.files" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
    </div>
  </div>
</template>

<script>
import { mapMutations, mapActions } from 'vuex';
import workspaceSvc from '../services/workspaceSvc';
import explorerSvc from '../services/explorerSvc';
import store from '../store';
import badgeSvc from '../services/badgeSvc';

export default {
  name: 'explorer-node', // Required for recursivity
  props: ['node', 'depth'],
  data: () => ({
    editingValue: '',
  }),
  computed: {
    leftPadding() {
      return `${this.depth * 15}px`;
    },
    childLeftPadding() {
      return `${(this.depth + 1) * 15}px`;
    },
    isSelected() {
      return store.getters['explorer/selectedNode'] === this.node;
    },
    isEditing() {
      return store.getters['explorer/editingNode'] === this.node;
    },
    isDragTarget() {
      return store.getters['explorer/dragTargetNode'] === this.node;
    },
    isDragTargetFolder() {
      return store.getters['explorer/dragTargetNodeFolder'] === this.node;
    },
    isOpen() {
      return store.state.explorer.openNodes[this.node.item.id] || this.node.isRoot;
    },
    newChild() {
      return store.getters['explorer/newChildNodeParent'] === this.node
        && store.state.explorer.newChildNode;
    },
    newChildName: {
      get() {
        return store.state.explorer.newChildNode.item.name;
      },
      set(value) {
        store.commit('explorer/setNewItemName', value);
      },
    },
    editingNodeName: {
      get() {
        return store.getters['explorer/editingNode'].item.name;
      },
      set(value) {
        this.editingValue = value.trim();
      },
    },
  },
  methods: {
    ...mapMutations('explorer', [
      'setEditingId',
    ]),
    ...mapActions('explorer', [
      'setDragTarget',
    ]),
    select(id = this.node.item.id, doOpen = true) {
      const node = store.getters['explorer/nodeMap'][id];
      if (!node) {
        return false;
      }
      store.commit('explorer/setSelectedId', id);
      if (doOpen) {
        // Prevent from freezing the UI while loading the file
        setTimeout(() => {
          if (node.isFolder) {
            store.commit('explorer/toggleOpenNode', id);
          } else if (store.state.file.currentId !== id) {
            store.commit('file/setCurrentId', id);
            badgeSvc.addBadge('switchFile');
          }
        }, 10);
      }
      return true;
    },
    async submitNewChild(cancel) {
      const { newChildNode } = store.state.explorer;
      if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
        try {
          if (newChildNode.isFolder) {
            const item = await workspaceSvc.storeItem(newChildNode.item);
            this.select(item.id);
            badgeSvc.addBadge('createFolder');
          } else {
            const item = await workspaceSvc.createFile(newChildNode.item);
            this.select(item.id);
            badgeSvc.addBadge('createFile');
          }
        } catch (e) {
          // Cancel
        }
      }
      store.commit('explorer/setNewItem', null);
    },
    async submitEdit(cancel) {
      const { item, isFolder } = store.getters['explorer/editingNode'];
      const value = this.editingValue;
      this.setEditingId(null);
      if (!cancel && item.id && value && item.name !== value) {
        try {
          await workspaceSvc.storeItem({
            ...item,
            name: value,
          });
          badgeSvc.addBadge(isFolder ? 'renameFolder' : 'renameFile');
        } catch (e) {
          // Cancel
        }
      }
    },
    setDragSourceId(evt) {
      if (this.node.noDrag) {
        evt.preventDefault();
        return;
      }
      store.commit('explorer/setDragSourceId', this.node.item.id);
      // Fix for Firefox
      // See https://stackoverflow.com/a/3977637/1333165
      evt.dataTransfer.setData('Text', '');
    },
    onDrop() {
      const sourceNode = store.getters['explorer/dragSourceNode'];
      const targetNode = store.getters['explorer/dragTargetNodeFolder'];
      this.setDragTarget();
      if (!sourceNode.isNil
        && !targetNode.isNil
        && sourceNode.item.id !== targetNode.item.id
      ) {
        workspaceSvc.storeItem({
          ...sourceNode.item,
          parentId: targetNode.item.id,
        });
        badgeSvc.addBadge(sourceNode.isFolder ? 'moveFolder' : 'moveFile');
      }
    },
    async onContextMenu(evt) {
      if (this.select(undefined, false)) {
        evt.preventDefault();
        evt.stopPropagation();
        const item = await store.dispatch('contextMenu/open', {
          coordinates: {
            left: evt.clientX,
            top: evt.clientY,
          },
          items: [{
            name: 'New file',
            disabled: !this.node.isFolder || this.node.isTrash,
            perform: () => explorerSvc.newItem(false),
          }, {
            name: 'New folder',
            disabled: !this.node.isFolder || this.node.isTrash || this.node.isTemp,
            perform: () => explorerSvc.newItem(true),
          }, {
            type: 'separator',
          }, {
            name: 'Rename',
            disabled: this.node.isTrash || this.node.isTemp,
            perform: () => this.setEditingId(this.node.item.id),
          }, {
            name: 'Delete',
            perform: () => explorerSvc.deleteItem(),
          }],
        });
        if (item) {
          item.perform();
        }
      }
    },
  },
};
</script>

<style lang="scss">
$item-font-size: 14px;

.explorer-node--drag-target {
  background-color: rgba(0, 128, 255, 0.2);
}

.explorer-node__item {
  position: relative;
  cursor: pointer;
  font-size: $item-font-size;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  padding-right: 5px;

  .explorer-node--selected > & {
    background-color: rgba(0, 0, 0, 0.2);

    .explorer__tree:focus & {
      background-color: #39f;
      color: #fff;
    }
  }

  .explorer__tree--new-item & {
    opacity: 0.33;
  }

  .explorer-node__location {
    float: right;
    width: 18px;
    height: 18px;
    margin: 2px 1px;
  }
}

.explorer-node--trash,
.explorer-node--temp {
  color: rgba(0, 0, 0, 0.5);
}

.explorer-node--folder > .explorer-node__item,
.explorer-node--folder > .explorer-node__item-editor,
.explorer-node__new-child--folder {
  &::before {
    content: '▹';
    position: absolute;
    margin-left: -13px;
  }
}

.explorer-node--folder.explorer-node--open > .explorer-node__item,
.explorer-node--folder.explorer-node--open > .explorer-node__item-editor {
  &::before {
    content: '▾';
  }
}

$new-child-height: 25px;

.explorer-node__item-editor,
.explorer-node__new-child {
  padding: 1px 10px;

  .text-input {
    font-size: $item-font-size;
    padding: 2px;
    height: $new-child-height;
  }
}
</style>


================================================
FILE: src/components/FindReplace.vue
================================================
<template>
  <div class="find-replace" @keydown.esc.stop="onEscape">
    <button class="find-replace__close-button button not-tabbable" @click="close()" v-title="'Close'">
      <icon-close></icon-close>
    </button>
    <div class="find-replace__row">
      <input type="text" class="find-replace__text-input find-replace__text-input--find text-input" @keydown.enter="find('forward')" v-model="findText">
      <div class="find-replace__find-stats">
        {{findPosition}} of {{findCount}}
      </div>
      <div class="flex flex--row flex--space-between">
        <div class="flex flex--row">
          <button class="find-replace__button find-replace__button--find-option button" :class="{'find-replace__button--on': findCaseSensitive}" @click="findCaseSensitive = !findCaseSensitive" title="Case sensitive">Aa</button>
          <button class="find-replace__button find-replace__button--find-option button" :class="{'find-replace__button--on': findUseRegexp}" @click="findUseRegexp = !findUseRegexp" title="Regular expression">.<sup>⁕</sup></button>
        </div>
        <div class="flex flex--row">
          <button class="find-replace__button button" @click="find('backward')">Previous</button>
          <button class="find-replace__button button" @click="find('forward')">Next</button>
        </div>
      </div>
    </div>
    <div v-if="type === 'replace'">
      <div class="find-replace__row">
        <input type="text" class="find-replace__text-input find-replace__text-input--replace text-input" @keydown.enter="replace" v-model="replaceText">
      </div>
      <div class="find-replace__row flex flex--row flex--end">
        <button class="find-replace__button button" @click="replace">Replace</button>
        <button class="find-replace__button button" @click="replaceAll">All</button>
      </div>
    </div>
  </div>
</template>

<script>
import { mapState } from 'vuex';
import editorSvc from '../services/editorSvc';
import cledit from '../services/editor/cledit';
import store from '../store';
import EditorClassApplier from './common/EditorClassApplier';

const accessor = (fieldName, setterName) => ({
  get() {
    return store.state.findReplace[fieldName];
  },
  set(value) {
    store.commit(`findReplace/${setterName}`, value);
  },
});

const computedLayoutSetting = key => ({
  get() {
    return store.getters['data/layoutSettings'][key];
  },
  set(value) {
    store.dispatch('data/patchLayoutSettings', {
      [key]: value,
    });
  },
});

class DynamicClassApplier {
  constructor(cssClass, offset, silent) {
    this.startMarker = new cledit.Marker(offset.start);
    this.endMarker = new cledit.Marker(offset.end);
    editorSvc.clEditor.addMarker(this.startMarker);
    editorSvc.clEditor.addMarker(this.endMarker);
    if (!silent) {
      this.classApplier = new EditorClassApplier(
        [`find-replace-${this.startMarker.id}`, cssClass],
        () => ({
          start: this.startMarker.offset,
          end: this.endMarker.offset,
        }),
      );
    }
  }

  clean = () => {
    editorSvc.clEditor.removeMarker(this.startMarker);
    editorSvc.clEditor.removeMarker(this.endMarker);
    if (this.classApplier) {
      this.classApplier.stop();
    }
  }
}

export default {
  data: () => ({
    findCount: 0,
    findPosition: 0,
  }),
  computed: {
    ...mapState('findReplace', [
      'type',
      'lastOpen',
    ]),
    findText: accessor('findText', 'setFindText'),
    replaceText: accessor('replaceText', 'setReplaceText'),
    findCaseSensitive: computedLayoutSetting('findCaseSensitive'),
    findUseRegexp: computedLayoutSetting('findUseRegexp'),
  },
  methods: {
    highlightOccurrences() {
      const oldClassAppliers = {};
      Object.entries(this.classAppliers).forEach(([, classApplier]) => {
        const newKey = `${classApplier.startMarker.offset}:${classApplier.endMarker.offset}`;
        oldClassAppliers[newKey] = classApplier;
      });
      const offsetList = [];
      this.classAppliers = {};
      if (this.state !== 'destroyed' && this.findText) {
        try {
          this.searchRegex = this.findText;
          if (!this.findUseRegexp) {
            this.searchRegex = this.searchRegex.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
          }
          this.replaceRegex = new RegExp(this.searchRegex, this.findCaseSensitive ? 'm' : 'mi');
          this.searchRegex = new RegExp(this.searchRegex, this.findCaseSensitive ? 'gm' : 'gmi');
          editorSvc.clEditor.getContent().replace(this.searchRegex, (...params) => {
            const match = params[0];
            const offset = params[params.length - 2];
            offsetList.push({
              start: offset,
              end: offset + match.length,
            });
          });
          offsetList.forEach((offset, i) => {
            const key = `${offset.start}:${offset.end}`;
            this.classAppliers[key] = oldClassAppliers[key] || new DynamicClassApplier(
              'find-replace-highlighting',
              offset,
              i > 200,
            );
          });
        } catch (e) {
          // Ignore
        }
        if (this.state !== 'created') {
          this.find('selection');
          this.state = 'created';
        }
      }
      Object.entries(oldClassAppliers).forEach(([key, classApplier]) => {
        if (!this.classAppliers[key]) {
          classApplier.clean();
          if (classApplier === this.selectedClassApplier) {
            this.selectedClassApplier.child.clean();
            this.selectedClassApplier = null;
          }
        }
      });
      this.findCount = offsetList.length;
    },
    unselectClassApplier() {
      if (this.selectedClassApplier) {
        this.selectedClassApplier.child.clean();
        this.selectedClassApplier.child = null;
        this.selectedClassApplier = null;
      }
      this.findPosition = 0;
    },
    find(mode = 'forward') {
      const { selectedClassApplier } = this;
      this.unselectClassApplier();
      const { selectionMgr } = editorSvc.clEditor;
      const startOffset = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
      const endOffset = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
      const keys = Object.keys(this.classAppliers);
      const finder = checker => (key) => {
        if (checker(this.classAppliers[key]) && selectedClassApplier !== this.classAppliers[key]) {
          this.selectedClassApplier = this.classAppliers[key];
          return true;
        }
        return false;
      };
      if (mode === 'backward') {
        this.selectedClassApplier = this.classAppliers[keys[keys.length - 1]];
        keys.reverse().some(finder(classApplier => classApplier.startMarker.offset <= startOffset));
      } else if (mode === 'selection') {
        keys.some(finder(classApplier => classApplier.startMarker.offset === startOffset &&
          classApplier.endMarker.offset === endOffset));
      } else if (mode === 'forward') {
        this.selectedClassApplier = this.classAppliers[keys[0]];
        keys.some(finder(classApplier => classApplier.endMarker.offset >= endOffset));
      }
      if (this.selectedClassApplier) {
        selectionMgr.setSelectionStartEnd(
          this.selectedClassApplier.startMarker.offset,
          this.selectedClassApplier.endMarker.offset,
        );
        this.selectedClassApplier.child = new DynamicClassApplier('find-replace-selection', {
          start: this.selectedClassApplier.startMarker.offset,
          end: this.selectedClassApplier.endMarker.offset,
        });
        selectionMgr.updateCursorCoordinates(this.$el.parentNode.clientHeight);
        // Deduce the findPosition
        Object.keys(this.classAppliers).forEach((key, i) => {
          if (this.selectedClassApplier !== this.classAppliers[key]) {
            return false;
          }
          this.findPosition = i + 1;
          return true;
        });
      }
    },
    replace() {
      if (this.searchRegex) {
        if (!this.selectedClassApplier) {
          this.find();
          return;
        }
        editorSvc.clEditor.replaceAll(
          this.replaceRegex,
          this.replaceText,
          this.selectedClassApplier.startMarker.offset,
        );
        this.$nextTick(() => this.find());
      }
    },
    replaceAll() {
      if (this.searchRegex) {
        editorSvc.clEditor.replaceAll(this.searchRegex, this.replaceText);
      }
    },
    close() {
      store.commit('findReplace/setType');
    },
    onEscape() {
      editorSvc.clEditor.focus();
    },
  },
  mounted() {
    this.classAppliers = {};

    // Highlight occurences
    this.debouncedHighlightOccurrences = cledit.Utils.debounce(
      () => this.highlightOccurrences(),
      25,
    );
    // Refresh highlighting when find text changes or changing options
    this.$watch(() => this.findText, this.debouncedHighlightOccurrences);
    this.$watch(() => this.findCaseSensitive, this.debouncedHighlightOccurrences);
    this.$watch(() => this.findUseRegexp, this.debouncedHighlightOccurrences);
    // Refresh highlighting when content changes
    editorSvc.clEditor.on('contentChanged', this.debouncedHighlightOccurrences);

    // Last open changes trigger focus on text input and find occurence in selection
    this.$watch(() => this.lastOpen, () => {
      const elt = this.$el.querySelector(`.find-replace__text-input--${this.type}`);
      elt.focus();
      elt.setSelectionRange(0, this[`${this.type}Text`].length);
      // Highlight and find in selection
      this.state = null;
      this.debouncedHighlightOccurrences();
    }, {
      immediate: true,
    });

    // Close on escape
    this.onKeyup = (evt) => {
      if (evt.which === 27) {
        // Esc key
        store.commit('findReplace/setType');
      }
    };
    window.addEventListener('keyup', this.onKeyup);

    // Unselect class applier when focus is out of the panel
    this.onFocusIn = () => this.$el.contains(document.activeElement) ||
      setTimeout(() => this.unselectClassApplier(), 15);
    window.addEventListener('focusin', this.onFocusIn);
  },
  destroyed() {
    // Unregister listeners
    editorSvc.clEditor.off('contentChanged', this.debouncedHighlightOccurrences);
    window.removeEventListener('keyup', this.onKeyup);
    window.removeEventListener('focusin', this.onFocusIn);
    this.state = 'destroyed';
    this.debouncedHighlightOccurrences();
  },
};
</script>

<style lang="scss">
@import '../styles/variables.scss';

.find-replace {
  padding: 0 35px 0 25px;
}

.find-replace__row {
  margin: 10px 0;
}

.find-replace__button {
  font-size: 15px;
  padding: 0 8px;
  line-height: 28px;
  height: 28px;
}

.find-replace__button--find-option {
  padding: 0;
  width: 28px;
  font-weight: 600;
  letter-spacing: -0.025em;
  color: rgba(0, 0, 0, 0.25);
  text-transform: none;

  &:active,
  &:focus,
  &:hover {
    color: rgba(0, 0, 0, 0.25);
  }
}

.find-replace__button--on {
  color: rgba(0, 0, 0, 0.67);

  &:active,
  &:focus,
  &:hover {
    color: rgba(0, 0, 0, 0.67);
  }
}

.find-replace__text-input {
  border: 1px solid transparent;
  padding: 2px 5px;
  height: 32px;

  &:focus {
    border-color: $link-color;
  }
}

.find-replace__close-button {
  position: absolute;
  top: 5px;
  right: 5px;
  width: 25px;
  height: 25px;
  padding: 2px;
  color: rgba(0, 0, 0, 0.5);

  &:active,
  &:focus,
  &:hover {
    color: rgba(0, 0, 0, 0.75);
  }
}

.find-replace__find-stats {
  text-align: right;
  font-size: 0.75em;
  opacity: 0.6;
}

.find-replace-highlighting {
  background-color: $highlighting-color;
  color: $editor-color-light !important;
}

.find-replace-selection {
  background-color: $selection-highlighting-color;
}
</style>


================================================
FILE: src/components/Layout.vue
================================================
<template>
  <div class="layout" :class="{'layout--revision': revisionContent}">
    <div class="layout__panel flex flex--row" :class="{'flex--end': styles.showSideBar}">
      <div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :aria-hidden="!styles.showExplorer" :style="{width: styles.layoutOverflow ? '100%' : constants.explorerWidth + 'px'}">
        <explorer></explorer>
      </div>
      <div class="layout__panel flex flex--column" tour-step-anchor="welcome,end" :style="{width: styles.innerWidth + 'px'}">
        <div class="layout__panel layout__panel--navigation-bar" v-show="styles.showNavigationBar" :style="{height: constants.navigationBarHeight + 'px'}">
          <navigation-bar></navigation-bar>
        </div>
        <div class="layout__panel flex flex--row" :style="{height: styles.innerHeight + 'px'}">
          <div class="layout__panel layout__panel--editor" v-show="styles.showEditor" :style="{width: (styles.editorWidth + styles.editorGutterWidth) + 'px', fontSize: styles.fontSize + 'px'}">
            <div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
              <div class="gutter__background" v-if="styles.editorGutterWidth" :style="{width: styles.editorGutterWidth + 'px'}"></div>
            </div>
            <editor></editor>
            <div class="gutter" :style="{left: styles.editorGutterLeft + 'px'}">
              <sticky-comment v-if="styles.editorGutterWidth && stickyComment === 'top'"></sticky-comment>
              <current-discussion v-if="styles.editorGutterWidth"></current-discussion>
            </div>
          </div>
          <div class="layout__panel layout__panel--button-bar" v-show="styles.showEditor" :style="{width: constants.buttonBarWidth + 'px'}">
            <button-bar></button-bar>
          </div>
          <div class="layout__panel layout__panel--preview" v-show="styles.showPreview" :style="{width: (styles.previewWidth + styles.previewGutterWidth) + 'px', fontSize: styles.fontSize + 'px'}">
            <div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
              <div class="gutter__background" v-if="styles.previewGutterWidth" :style="{width: styles.previewGutterWidth + 'px'}"></div>
            </div>
            <preview></preview>
            <div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
              <sticky-comment v-if="styles.previewGutterWidth && stickyComment === 'top'"></sticky-comment>
              <current-discussion v-if="styles.previewGutterWidth"></current-discussion>
            </div>
          </div>
          <div class="layout__panel layout__panel--find-replace" v-if="showFindReplace">
            <find-replace></find-replace>
          </div>
        </div>
        <div class="layout__panel layout__panel--status-bar" v-show="styles.showStatusBar" :style="{height: constants.statusBarHeight + 'px'}">
          <status-bar></status-bar>
        </div>
      </div>
      <div class="layout__panel layout__panel--side-bar" v-show="styles.showSideBar" :style="{width: styles.layoutOverflow ? '100%' : constants.sideBarWidth + 'px'}">
        <side-bar></side-bar>
      </div>
    </div>
    <tour v-if="!light && !layoutSettings.welcomeTourFinished"></tour>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import NavigationBar from './NavigationBar';
import ButtonBar from './ButtonBar';
import StatusBar from './StatusBar';
import Explorer from './Explorer';
import SideBar from './SideBar';
import Editor from './Editor';
import Preview from './Preview';
import Tour from './Tour';
import StickyComment from './gutters/StickyComment';
import CurrentDiscussion from './gutters/CurrentDiscussion';
import FindReplace from './FindReplace';
import editorSvc from '../services/editorSvc';
import markdownConversionSvc from '../services/markdownConversionSvc';
import store from '../store';

export default {
  components: {
    NavigationBar,
    ButtonBar,
    StatusBar,
    Explorer,
    SideBar,
    Editor,
    Preview,
    Tour,
    StickyComment,
    CurrentDiscussion,
    FindReplace,
  },
  computed: {
    ...mapState([
      'light',
    ]),
    ...mapState('content', [
      'revisionContent',
    ]),
    ...mapState('discussion', [
      'stickyComment',
    ]),
    ...mapGetters('layout', [
      'constants',
      'styles',
    ]),
    ...mapGetters('data', [
      'layoutSettings',
    ]),
    showFindReplace() {
      return !!store.state.findReplace.type;
    },
  },
  methods: {
    ...mapActions('layout', [
      'updateBodySize',
    ]),
    saveSelection: () => editorSvc.saveSelection(true),
  },
  created() {
    markdownConversionSvc.init(); // Needs to be inited before mount
    this.updateBodySize();
    window.addEventListener('resize', this.updateBodySize);
    window.addEventListener('keyup', this.saveSelection);
    window.addEventListener('mouseup', this.saveSelection);
    window.addEventListener('focusin', this.saveSelection);
    window.addEventListener('contextmenu', this.saveSelection);
  },
  mounted() {
    const editorElt = this.$el.querySelector('.editor__inner');
    const previewElt = this.$el.querySelector('.preview__inner-2');
    const tocElt = this.$el.querySelector('.toc__inner');
    editorSvc.init(editorElt, previewElt, tocElt);

    // Focus on the editor every time reader mode is disabled
    const focus = () => {
      if (this.styles.showEditor) {
        editorSvc.clEditor.focus();
      }
    };
    setTimeout(focus, 100);
    this.$watch(() => this.styles.showEditor, focus);
  },
  destroyed() {
    window.removeEventListener('resize', this.updateStyle);
    window.removeEventListener('keyup', this.saveSelection);
    window.removeEventListener('mouseup', this.saveSelection);
    window.removeEventListener('focusin', this.saveSelection);
    window.removeEventListener('contextmenu', this.saveSelection);
  },
};
</script>

<style lang="scss">
@import '../styles/variables.scss';

.layout {
  position: absolute;
  width: 100%;
  height: 100%;
}

.layout__panel {
  position: relative;
  width: 100%;
  height: 100%;
  flex: none;
  overflow: hidden;
}

.layout__panel--navigation-bar {
  background-color: $navbar-bg;
}

.layout__panel--status-bar {
  background-color: #007acc;
}

.layout__panel--editor {
  background-color: $editor-background-light;

  .app--dark & {
    background-color: $editor-background-dark;
  }

  .gutter__background,
  .comment-list__current-discussion,
  .sticky-comment,
  .current-discussion {
    background-color: mix(#000, $editor-background-light, 6.7%);

    .app--dark & {
      background-color: mix(#fff, $editor-background-dark, 6.7%);
    }
  }
}

$preview-background-light: #f3f3f3;
$preview-background-dark: #252525;

.layout__panel--preview,
.layout__panel--button-bar {
  background-color: $preview-background-light;

  .app--dark & {
    background-color: $preview-background-dark;
  }
}

.layout__panel--preview {
  .gutter__background,
  .comment-list__current-discussion,
  .sticky-comment,
  .current-discussion {
    background-color: mix(#000, $preview-background-light, 6.7%);
  }
}

.layout__panel--explorer,
.layout__panel--side-bar {
  background-color: #ddd;
}

.layout__panel--find-replace {
  background-color: #e6e6e6;
  position: absolute;
  left: 0;
  bottom: 0;
  width: 300px;
  height: auto;
  border-top-right-radius: $border-radius-base;
}
</style>


================================================
FILE: src/components/Modal.vue
================================================
<template>
  <div class="modal" v-if="config" @keydown.esc.stop="onEscape" @keydown.tab="onTab" @focusin="onFocusInOut" @focusout="onFocusInOut">
    <div class="modal__sponsor-banner" v-if="!isSponsor">
      StackEdit is <a class="not-tabbable" target="_blank" href="https://github.com/benweet/stackedit/">open source</a>, please consider
      <a class="not-tabbable" href="javascript:void(0)" @click="sponsor">sponsoring</a> for just $5.
    </div>
    <component v-if="currentModalComponent" :is="currentModalComponent"></component>
    <modal-inner v-else aria-label="Dialog">
      <div class="modal__content" v-html="simpleModal.contentHtml(config)"></div>
      <div class="modal__button-bar">
        <button class="button" v-if="simpleModal.rejectText" @click="config.reject()">{{simpleModal.rejectText}}</button>
        <button class="button button--resolve" v-if="simpleModal.resolveText" @click="config.resolve()">{{simpleModal.resolveText}}</button>
      </div>
    </modal-inner>
  </div>
</template>

<script>
import { mapGetters } from 'vuex';
import simpleModals from '../data/simpleModals';
import editorSvc from '../services/editorSvc';
import syncSvc from '../services/syncSvc';
import googleHelper from '../services/providers/helpers/googleHelper';
import store from '../store';

import ModalInner from './modals/common/ModalInner';
import FilePropertiesModal from './modals/FilePropertiesModal';
import SettingsModal from './modals/SettingsModal';
import TemplatesModal from './modals/TemplatesModal';
import AboutModal from './modals/AboutModal';
import HtmlExportModal from './modals/HtmlExportModal';
import PdfExportModal from './modals/PdfExportModal';
import PandocExportModal from './modals/PandocExportModal';
import LinkModal from './modals/LinkModal';
import ImageModal from './modals/ImageModal';
import SyncManagementModal from './modals/SyncManagementModal';
import PublishManagementModal from './modals/PublishManagementModal';
import WorkspaceManagementModal from './modals/WorkspaceManagementModal';
import AccountManagementModal from './modals/AccountManagementModal';
import BadgeManagementModal from './modals/BadgeManagementModal';
import SponsorModal from './modals/SponsorModal';

// Providers
import GooglePhotoModal from './modals/providers/GooglePhotoModal';
import GoogleDriveAccountModal from './modals/providers/GoogleDriveAccountModal';
import GoogleDriveSaveModal from './modals/providers/GoogleDriveSaveModal';
import GoogleDriveWorkspaceModal from './modals/providers/GoogleDriveWorkspaceModal';
import GoogleDrivePublishModal from './modals/providers/GoogleDrivePublishModal';
import DropboxAccountModal from './modals/providers/DropboxAccountModal';
import DropboxSaveModal from './modals/providers/DropboxSaveModal';
import DropboxPublishModal from './modals/providers/DropboxPublishModal';
import GithubAccountModal from './modals/providers/GithubAccountModal';
import GithubOpenModal from './modals/providers/GithubOpenModal';
import GithubSaveModal from './modals/providers/GithubSaveModal';
import GithubWorkspaceModal from './modals/providers/GithubWorkspaceModal';
import GithubPublishModal from './modals/providers/GithubPublishModal';
import GistSyncModal from './modals/providers/GistSyncModal';
import GistPublishModal from './modals/providers/GistPublishModal';
import GitlabAccountModal from './modals/providers/GitlabAccountModal';
import GitlabOpenModal from './modals/providers/GitlabOpenModal';
import GitlabPublishModal from './modals/providers/GitlabPublishModal';
import GitlabSaveModal from './modals/providers/GitlabSaveModal';
import GitlabWorkspaceModal from './modals/providers/GitlabWorkspaceModal';
import WordpressPublishModal from './modals/providers/WordpressPublishModal';
import BloggerPublishModal from './modals/providers/BloggerPublishModal';
import BloggerPagePublishModal from './modals/providers/BloggerPagePublishModal';
import ZendeskAccountModal from './modals/providers/ZendeskAccountModal';
import ZendeskPublishModal from './modals/providers/ZendeskPublishModal';
import CouchdbWorkspaceModal from './modals/providers/CouchdbWorkspaceModal';
import CouchdbCredentialsModal from './modals/providers/CouchdbCredentialsModal';

const getTabbables = container => container.querySelectorAll('a[href], button, .textfield, input[type=checkbox]')
  // Filter enabled and visible element
  .cl_filter(el => !el.disabled && el.offsetParent !== null && !el.classList.contains('not-tabbable'));

export default {
  components: {
    ModalInner,
    FilePropertiesModal,
    SettingsModal,
    TemplatesModal,
    AboutModal,
    HtmlExportModal,
    PdfExportModal,
    PandocExportModal,
    LinkModal,
    ImageModal,
    SyncManagementModal,
    PublishManagementModal,
    WorkspaceManagementModal,
    AccountManagementModal,
    BadgeManagementModal,
    SponsorModal,
    // Providers
    GooglePhotoModal,
    GoogleDriveAccountModal,
    GoogleDriveSaveModal,
    GoogleDriveWorkspaceModal,
    GoogleDrivePublishModal,
    DropboxAccountModal,
    DropboxSaveModal,
    DropboxPublishModal,
    GithubAccountModal,
    GithubOpenModal,
    GithubSaveModal,
    GithubWorkspaceModal,
    GithubPublishModal,
    GistSyncModal,
    GistPublishModal,
    GitlabAccountModal,
    GitlabOpenModal,
    GitlabPublishModal,
    GitlabSaveModal,
    GitlabWorkspaceModal,
    WordpressPublishModal,
    BloggerPublishModal,
    BloggerPagePublishModal,
    ZendeskAccountModal,
    ZendeskPublishModal,
    CouchdbWorkspaceModal,
    CouchdbCredentialsModal,
  },
  computed: {
    ...mapGetters([
      'isSponsor',
    ]),
    ...mapGetters('modal', [
      'config',
    ]),
    currentModalComponent() {
      if (this.config.type) {
        let componentName = this.config.type[0].toUpperCase();
        componentName += this.config.type.slice(1);
        componentName += 'Modal';
        if (this.$options.components[componentName]) {
          return componentName;
        }
      }
      return null;
    },
    simpleModal() {
      return simpleModals[this.config.type] || {};
    },
  },
  methods: {
    async sponsor() {
      try {
        if (!store.getters['workspace/sponsorToken']) {
          // User has to sign in
          await store.dispatch('modal/open', 'signInForSponsorship');
          await googleHelper.signin();
          syncSvc.requestSync();
        }
        if (!store.getters.isSponsor) {
          await store.dispatch('modal/open', 'sponsor');
        }
      } catch (e) { /* cancel */ }
    },
    onEscape() {
      this.config.reject();
      editorSvc.clEditor.focus();
    },
    onTab(evt) {
      const tabbables = getTabbables(this.$el);
      const firstTabbable = tabbables[0];
      const lastTabbable = tabbables[tabbables.length - 1];
      if (evt.shiftKey && firstTabbable === evt.target) {
        evt.preventDefault();
        lastTabbable.focus();
      } else if (!evt.shiftKey && lastTabbable === evt.target) {
        evt.preventDefault();
        firstTabbable.focus();
      }
    },
    onFocusInOut(evt) {
      const { parentNode } = evt.target;
      if (parentNode && parentNode.parentNode) {
        // Focus effect
        if (parentNode.classList.contains('form-entry__field')
          && parentNode.parentNode.classList.contains('form-entry')) {
          parentNode.parentNode.classList.toggle(
            'form-entry--focused',
            evt.type === 'focusin',
          );
        }
      }
    },
  },
  mounted() {
    this.$watch(
      () => this.config,
      (isOpen) => {
        if (isOpen) {
          const tabbables = getTabbables(this.$el);
          if (tabbables[0]) {
            tabbables[0].focus();
          }
        }
      },
      { immediate: true },
    );
  },
};
</script>

<style lang="scss">
@import '../styles/variables.scss';

.modal {
  position: absolute;
  width: 100%;
  height: 100%;
  background-color: rgba(160, 160, 160, 0.5);
  overflow: auto;

  p {
    line-height: 1.5;
  }
}

.modal__sponsor-banner {
  position: fixed;
  z-index: 1;
  width: 100%;
  color: darken($error-color, 10%);
  background-color: transparentize(lighten($error-color, 33%), 0.075);
  font-size: 0.9em;
  line-height: 1.33;
  text-align: center;
  padding: 0.25em 1em;
}

.modal__inner-1 {
  margin: 0 auto;
  width: 100%;
  min-width: 320px;
  max-width: 480px;
}

.modal__inner-2 {
  margin: 40px 10px 100px;
  background-color: #f8f8f8;
  padding: 50px 50px 40px;
  border-radius: $border-radius-base;
  position: relative;
  overflow: hidden;

  &::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    height: $border-radius-base;
    width: 100%;
    background-image: linear-gradient(to left, #ffd700, #ffd700 23%, #a5c700 27%, #a5c700 48%, #ff8a00 52%, #ff8a00 73%, #66aefd 77%);
  }

  &::after {
    content: '';
    position: absolute;
    bottom: 0;
    left: 0;
    height: $border-radius-base;
    width: 100%;
    background-image: linear-gradient(to right, #ffd700, #ffd700 23%, #a5c700 27%, #a5c700 48%, #ff8a00 52%, #ff8a00 73%, #66aefd 77%);
  }
}

.modal__content > :first-child,
.modal__content > .modal__image:first-child + * {
  margin-top: 0;
}

.modal__image {
  float: left;
  width: 60px;
  height: 60px;
  margin: 1.5em 1.2em 0.5em 0;

  & + *::after {
    content: '';
    display: block;
    clear: both;
  }
}

.modal__title {
  font-weight: bold;
  font-size: 1.5rem;
  line-height: 1.4;
  margin-top: 2.5rem;
}

.modal__sub-title {
  opacity: 0.6;
  font-size: 0.75rem;
  margin-bottom: 1.5rem;
}

.modal__error {
  color: #de2c00;
}

.modal__info {
  background-color: $info-bg;
  border-radius: $border-radius-base;
  margin: 1.2em 0;
  padding: 0.75em 1.25em;
  font-size: 0.95em;
  line-height: 1.6;

  pre {
    line-height: 1.5;
  }
}

.modal__info--multiline {
  padding-top: 0.1em;
  padding-bottom: 0.1em;
}

.modal__button-bar {
  margin-top: 2rem;
  display: flex;
  flex-direction: row;
  justify-content: flex-end;
}

.form-entry {
  margin: 1em 0;
}

.form-entry__label {
  display: block;
  font-size: 0.9rem;
  color: #808080;

  .form-entry--focused & {
    color: darken($link-color, 10%);
  }

  .form-entry--error & {
    color: darken($error-color, 10%);
  }
}

.form-entry__label-info {
  font-size: 0.75rem;
}

.form-entry__field {
  border: 1px solid #b0b0b0;
  border-radius: $border-radius-base;
  position: relative;
  overflow: hidden;

  .form-entry--focused & {
    border-color: $link-color;
    box-shadow: 0 0 0 2.5px transparentize($link-color, 0.67);
  }

  .form-entry--error & {
    border-color: $error-color;
    box-shadow: 0 0 0 2.5px transparentize($error-color, 0.67);
  }
}

.form-entry__actions {
  text-align: right;
  margin: 0.25em;
}

.form-entry__button {
  width: 38px;
  height: 38px;
  padding: 6px;
  display: inline-block;
  background-color: transparent;
  opacity: 0.75;

  &:active,
  &:focus,
  &:hover {
    opacity: 1;
    background-color: rgba(0, 0, 0, 0.1);
  }
}

.form-entry__radio,
.form-entry__checkbox {
  margin: 0.25em 1em;

  input {
    margin-right: 0.25em;
  }
}

.form-entry__info {
  font-size: 0.75em;
  opacity: 0.67;
  line-height: 1.4;
  margin: 0.25em 0;
}

.tabs {
  border-bottom: 1px solid $hr-color;
  margin: 1em 0 2em;

  &::after {
    content: '';
    display: block;
    clear: both;
  }
}

.tabs__tab {
  width: 50%;
  float: left;
  text-align: center;
  line-height: 1.4;
  font-weight: 400;
  font-size: 1.1em;
}

.tabs__tab > a {
  width: 100%;
  text-decoration: none;
  padding: 0.67em 0.33em;
  cursor: pointer;
  border-bottom: 2px solid transparent;
  border-top-left-radius: $border-radius-base;
  border-top-right-radius: $border-radius-base;
  color: $link-color;

  &:hover,
  &:focus {
    background-color: rgba(0, 0, 0, 0.05);
  }
}

.tabs__tab--active > a {
  border-bottom: 2px solid $link-color;
  color: inherit;
}
</style>


================================================
FILE: src/components/NavigationBar.vue
================================================
<template>
  <nav class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor && !revisionContent, 'navigation-bar--light': light}">
    <!-- Explorer -->
    <div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
      <button class="navigation-bar__button navigation-bar__button--close button" v-if="light" @click="close()" v-title="'Close StackEdit'"><icon-check-circle></icon-check-circle></button>
      <button class="navigation-bar__button navigation-bar__button--explorer-toggler button" v-else tour-step-anchor="explorer" @click="toggleExplorer()" v-title="'Toggle explorer'"><icon-folder></icon-folder></button>
    </div>
    <!-- Side bar -->
    <div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
      <a class="navigation-bar__button navigation-bar__button--stackedit button" v-if="light" href="app" target="_blank" v-title="'Open StackEdit'"><icon-provider provider-id="stackedit"></icon-provider></a>
      <button class="navigation-bar__button navigation-bar__button--stackedit button" v-else tour-step-anchor="menu" @click="toggleSideBar()" v-title="'Toggle side bar'"><icon-provider provider-id="stackedit"></icon-provider></button>
    </div>
    <div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--title flex flex--row">
      <!-- Spinner -->
      <div class="navigation-bar__spinner">
        <div v-if="!offline && showSpinner" class="spinner"></div>
        <icon-sync-off v-if="offline"></icon-sync-off>
      </div>
      <!-- Title -->
      <div class="navigation-bar__title navigation-bar__title--fake text-input"></div>
      <div class="navigation-bar__title navigation-bar__title--text text-input" :style="{width: titleWidth + 'px'}">{{title}}</div>
      <input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" :style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keydown.enter="submitTitle(false)" @keydown.esc.stop="submitTitle(true)" @mouseenter="titleHover = true" @mouseleave="titleHover = false" v-model="title">
      <!-- Sync/Publish -->
      <div class="flex flex--row" :class="{'navigation-bar__hidden': styles.hideLocations}">
        <a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in syncLocations" :key="location.id" :href="location.url" target="_blank" v-title="'Synchronized location'"><icon-provider :provider-id="location.providerId"></icon-provider></a>
        <button class="navigation-bar__button navigation-bar__button--sync button" :disabled="!isSyncPossible || isSyncRequested || offline" @click="requestSync" v-title="'Synchronize now'"><icon-sync></icon-sync></button>
        <a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in publishLocations" :key="location.id" :href="location.url" target="_blank" v-title="'Publish location'"><icon-provider :provider-id="location.providerId"></icon-provider></a>
        <button class="navigation-bar__button navigation-bar__button--publish button" :disabled="!publishLocations.length || isPublishRequested || offline" @click="requestPublish" v-title="'Publish now'"><icon-upload></icon-upload></button>
      </div>
      <!-- Revision -->
      <div class="flex flex--row" v-if="revisionContent">
        <button class="navigation-bar__button navigation-bar__button--revision navigation-bar__button--restore button" @click="restoreRevision">Restore</button>
        <button class="navigation-bar__button navigation-bar__button--revision button" @click="setRevisionContent()" v-title="'Close revision'"><icon-close></icon-close></button>
      </div>
    </div>
    <div class="navigation-bar__inner navigation-bar__inner--edit-pagedownButtons">
      <button class="navigation-bar__button button" @click="undo" v-title="'Undo'" :disabled="!canUndo"><icon-undo></icon-undo></button>
      <button class="navigation-bar__button button" @click="redo" v-title="'Redo'" :disabled="!canRedo"><icon-redo></icon-redo></button>
      <div v-for="button in pagedownButtons" :key="button.method">
        <button class="navigation-bar__button button" v-if="button.method" @click="pagedownClick(button.method)" v-title="button.titleWithShortcut">
          <component :is="button.iconClass"></component>
        </button>
        <div class="navigation-bar__spacer" v-else></div>
      </div>
    </div>
  </nav>
</template>

<script>
import { mapState, mapMutations, mapGetters, mapActions } from 'vuex';
import editorSvc from '../services/editorSvc';
import syncSvc from '../services/syncSvc';
import publishSvc from '../services/publishSvc';
import animationSvc from '../services/animationSvc';
import tempFileSvc from '../services/tempFileSvc';
import utils from '../services/utils';
import pagedownButtons from '../data/pagedownButtons';
import store from '../store';
import workspaceSvc from '../services/workspaceSvc';
import badgeSvc from '../services/badgeSvc';

// According to mousetrap
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';

const getShortcut = (method) => {
  let result = '';
  Object.entries(store.getters['data/computedSettings'].shortcuts).some(([keys, shortcut]) => {
    if (`${shortcut.method || shortcut}` === method) {
      result = keys.split('+').map(key => key.toLowerCase()).map((key) => {
        if (key === 'mod') {
          return mod;
        }
        // Capitalize
        return key && `${key[0].toUpperCase()}${key.slice(1)}`;
      }).join('+');
    }
    return result;
  });
  return result && ` – ${result}`;
};

export default {
  data: () => ({
    mounted: false,
    title: '',
    titleFocus: false,
    titleHover: false,
  }),
  computed: {
    ...mapState([
      'light',
      'offline',
    ]),
    ...mapState('queue', [
      'isSyncRequested',
      'isPublishRequested',
      'currentLocation',
    ]),
    ...mapState('layout', [
      'canUndo',
      'canRedo',
    ]),
    ...mapState('content', [
      'revisionContent',
    ]),
    ...mapGetters('layout', [
      'styles',
    ]),
    ...mapGetters('syncLocation', {
      syncLocations: 'current',
    }),
    ...mapGetters('publishLocation', {
      publishLocations: 'current',
    }),
    pagedownButtons() {
      return pagedownButtons.map(button => ({
        ...button,
        titleWithShortcut: `${button.title}${getShortcut(button.method)}`,
        iconClass: `icon-${button.icon}`,
      }));
    },
    isSyncPossible() {
      return store.getters['workspace/syncToken'] ||
        store.getters['syncLocation/current'].length;
    },
    showSpinner() {
      return !store.state.queue.isEmpty;
    },
    titleWidth() {
      if (!this.mounted) {
        return 0;
      }
      this.titleFakeElt.textContent = this.title;
      const width = this.titleFakeElt.getBoundingClientRect().width + 2; // 2px for the caret
      return Math.min(width, this.styles.titleMaxWidth);
    },
    titleScrolling() {
      const result = this.titleHover && !this.titleFocus;
      if (this.titleInputElt) {
        if (result) {
          const scrollLeft = this.titleInputElt.scrollWidth - this.titleInputElt.offsetWidth;
          animationSvc.animate(this.titleInputElt)
            .scrollLeft(scrollLeft)
            .duration(scrollLeft * 10)
            .easing('inOut')
            .start();
        } else {
          animationSvc.animate(this.titleInputElt)
            .scrollLeft(0)
            .start();
        }
      }
      return result;
    },
    editCancelTrigger() {
      const current = store.getters['file/current'];
      return utils.serializeObject([
        current.id,
        current.name,
      ]);
    },
  },
  methods: {
    ...mapMutations('content', [
      'setRevisionContent',
    ]),
    ...mapActions('content', [
      'restoreRevision',
    ]),
    ...mapActions('data', [
      'toggleExplorer',
      'toggleSideBar',
    ]),
    undo() {
      return editorSvc.clEditor.undoMgr.undo();
    },
    redo() {
      return editorSvc.clEditor.undoMgr.redo();
    },
    requestSync() {
      if (this.isSyncPossible && !this.isSyncRequested) {
        syncSvc.requestSync(true);
      }
    },
    requestPublish() {
      if (this.publishLocations.length && !this.isPublishRequested) {
        publishSvc.requestPublish();
      }
    },
    pagedownClick(name) {
      if (store.getters['content/isCurrentEditable']) {
        const text = editorSvc.clEditor.getContent();
        editorSvc.pagedownEditor.uiManager.doClick(name);
        if (text !== editorSvc.clEditor.getContent()) {
          badgeSvc.addBadge('formatButtons');
        }
      }
    },
    async editTitle(toggle) {
      this.titleFocus = toggle;
      if (toggle) {
        this.titleInputElt.setSelectionRange(0, this.titleInputElt.value.length);
      } else {
        const title = this.title.trim();
        this.title = store.getters['file/current'].name;
        if (title && this.title !== title) {
          try {
            await workspaceSvc.storeItem({
              ...store.getters['file/current'],
              name: title,
            });
            badgeSvc.addBadge('editCurrentFileName');
          } catch (e) {
            // Cancel
          }
        }
      }
    },
    submitTitle(reset) {
      if (reset) {
        this.title = '';
      }
      this.titleInputElt.blur();
    },
    close() {
      tempFileSvc.close();
    },
  },
  created() {
    this.$watch(
      () => this.editCancelTrigger,
      () => {
        this.title = '';
        this.editTitle(false);
      },
      { immediate: true },
    );
  },
  mounted() {
    this.titleFakeElt = this.$el.querySelector('.navigation-bar__title--fake');
    this.titleInputElt = this.$el.querySelector('.navigation-bar__title--input');
    this.mounted = true;
  },
};
</script>

<style lang="scss">
@import '../styles/variables.scss';

.navigation-bar {
  position: absolute;
  width: 100%;
  height: 100%;
  padding-top: 4px;
  overflow: hidden;
}

.navigation-bar__hidden {
  display: none;
}

.navigation-bar__inner--left {
  float: left;

  &.navigation-bar__inner--button {
    margin-right: 12px;
  }
}

.navigation-bar__inner--right {
  float: right;

  /* prevent from seeing wrapped pagedownButtons */
  margin-bottom: 20px;
}

.navigation-bar__inner--button {
  margin: 0 4px;
}

.navigation-bar__inner--edit-pagedownButtons {
  margin-left: 15px;

  .navigation-bar__button,
  .navigation-bar__spacer {
    float: left;
  }
}

.navigation-bar__inner--title * {
  flex: none;
}

.navigation-bar__button,
.navigation-bar__spacer {
  height: 36px;
  padding: 0 4px;

  /* prevent from seeing wrapped pagedownButtons */
  margin-bottom: 20px;
}

.navigation-bar__button {
  width: 34px;
  padding: 0 7px;
  transition: opacity 0.25s;

  .navigation-bar__inner--button & {
    padding: 0 4px;
    width: 38px;

    &.navigation-bar__button--stackedit {
      opacity: 0.85;

      &:active,
      &:focus,
      &:hover {
        opacity: 1;
      }
    }
  }
}

.navigation-bar__button--revision {
  width: 38px;

  &:first-child {
    margin-left: 10px;
  }

  &:last-child {
    margin-right: 10px;
  }
}

.navigation-bar__button--restore {
  width: auto;
}

.navigation-bar__title {
  margin: 0 4px;
  font-size: 21px;

  .layout--revision & {
    position: absolute;
    left: -9999px;
  }
}

.navigation-bar__title,
.navigation-bar__button {
  display: inline-block;
  color: $navbar-color;
  background-color: transparent;
}

.navigation-bar__button--sync,
.navigation-bar__button--publish {
  padding: 0 6px;
  margin: 0 5px;
}

.navigation-bar__button[disabled] {
  &,
  &:active,
  &:focus,
  &:hover {
    color: $navbar-color;
  }
}

.navigation-bar__title--input,
.navigation-bar__button {
  &:active,
  &:focus,
  &:hover {
    color: $navbar-hover-color;
    background-color: $navbar-hover-background;
  }
}

.navigation-bar__button--location {
  width: 20px;
  height: 20px;
  border-radius: 10px;
  padding: 2px;
  margin-top: 8px;
  opacity: 0.5;
  background-color: rgba(255, 255, 255, 0.2);

  &:active,
  &:focus,
  &:hover {
    opacity: 1;
    background-color: rgba(255, 255, 255, 0.2);
  }
}

.navigation-bar__button--blink {
  animation: blink 1s linear infinite;
}

.navigation-bar__title--fake {
  position: absolute;
  left: -9999px;
  width: auto;
  white-space: pre-wrap;
}

.navigation-bar__title--text {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;

  .navigation-bar--editor & {
    display: none;
  }
}

.navigation-bar__title--input,
.navigation-bar__inner--edit-pagedownButtons {
  display: none;

  .navigation-bar--editor & {
    display: block;
  }
}

.navigation-bar__button {
  display: none;

  .navigation-bar__inner--button &,
  .navigation-bar--editor & {
    display: inline-block;
  }
}

.navigation-bar__button--revision {
  display: inline-block;
}

.navigation-bar__button--close {
  color: lighten($link-color, 15%);

  &:active,
  &:focus,
  &:hover {
    color: lighten($link-color, 25%);
  }
}

.navigation-bar__title--input {
  cursor: pointer;

  &.navigation-bar__title--focus {
    cursor: text;
  }

  .navigation-bar--light & {
    display: none;
  }
}

$r: 10px;
$d: $r * 2;
$b: $d/10;
$t: 3000ms;

.navigation-bar__spinner {
  width: 24px;
  margin: 7px 0 0 8px;

  .icon {
    width: 24px;
    height: 24px;
    color: transparentize($error-color, 0.5);
  }
}

.spinner {
  width: $d;
  height: $d;
  display: block;
  position: relative;
  border: $b solid transparentize($navbar-color, 0.5);
  border-radius: 50%;
  margin: 2px;

  &::before,
  &::after {
    content: "";
    position: absolute;
    display: block;
    width: $b;
    background-color: $navbar-color;
    border-radius: $b * 0.5;
    transform-origin: 50% 0;
  }

  &::before {
    height: $r * 0.4;
    left: $r - $b * 1.5;
    top: 50%;
    animation: spin $t linear infinite;
  }

  &::after {
    height: $r * 0.6;
    left: $r - $b * 1.5;
    top: 50%;
    animation: spin $t/4 linear infinite;
  }
}

@keyframes spin {
  to {
    transform: rotate(360deg);
  }
}

@keyframes blink {
  50% {
    opacity: 1;
  }
}
</style>


================================================
FILE: src/components/Notification.vue
================================================
<template>
  <div class="notification">
    <div class="notification__item flex flex--row flex--align-center" v-for="(item, idx) in items" :key="idx">
      <div class="notification__icon flex flex--column flex--center">
        <icon-alert v-if="item.type === 'error'"></icon-alert>
        <icon-check-circle v-else-if="item.type === 'badge'"></icon-check-circle>
        <icon-information v-else></icon-information>
      </div>
      <div class="notification__content">
        {{item.content}}
      </div>
      <button class="notification__button button" v-if="item.type === 'confirm'" @click="item.reject">
        No
      </button>
      <button class="notification__button button" v-if="item.type === 'confirm'" @click="item.resolve">
        Yes
      </button>
    </div>
  </div>
</template>

<script>
import { mapState } from 'vuex';

export default {
  computed: mapState('notification', [
    'items',
  ]),
};
</script>

<style lang="scss">
@import '../styles/variables.scss';

.notification {
  position: absolute;
  bottom: 0;
  right: 0;
  width: 100%;
  max-width: 340px;
}

.notification__item {
  margin: 10px;
  padding: 10px 15px;
  line-height: 1.4;
  background-color: #000;
  color: #fff;
  font-size: 0.9em;
  border-radius: $border-radius-base;
}

.notification__icon {
  height: 20px;
  width: 20px;
  margin-right: 12px;
  flex: none;
}

.notification__button {
  color: $navbar-color;
  padding: 8px;
  flex: none;

  &:active,
  &:focus,
  &:hover {
    color: $navbar-hover-color;
    background-color: $navbar-hover-background;
  }
}
</style>


================================================
FILE: src/components/Preview.vue
================================================
<template>
  <div class="preview">
    <div class="preview__inner-1" @click="onClick" @scroll="onScroll">
      <div class="preview__inner-2" :style="{padding: styles.previewPadding}">
      </div>
      <div class="gutter" :style="{left: styles.previewGutterLeft + 'px'}">
        <comment-list v-if="styles.previewGutterWidth"></comment-list>
        <preview-new-discussion-button v-if="!isCurrentTemp"></preview-new-discussion-button>
      </div>
    </div>
    <div v-if="!styles.showEditor" class="preview__corner">
      <button class="preview__button button" @click="toggleEditor(true)" v-title="'Edit file'">
        <icon-pen></icon-pen>
      </button>
    </div>
  </div>
</template>


<script>
import { mapGetters, mapActions } from 'vuex';
import CommentList from './gutters/CommentList';
import PreviewNewDiscussionButton from './gutters/PreviewNewDiscussionButton';
import store from '../store';

const appUri = `${window.location.protocol}//${window.location.host}`;

export default {
  components: {
    CommentList,
    PreviewNewDiscussionButton,
  },
  data: () => ({
    previewTop: true,
  }),
  computed: {
    ...mapGetters('file', [
      'isCurrentTemp',
    ]),
    ...mapGetters('layout', [
      'styles',
    ]),
  },
  methods: {
    ...mapActions('data', [
      'toggleEditor',
    ]),
    onClick(evt) {
      let elt = evt.target;
      while (elt !== this.$el) {
        if (elt.href && elt.href.match(/^https?:\/\//)
          && (!elt.hash || elt.href.slice(0, appUri.length) !== appUri)) {
          evt.preventDefault();
          const wnd = window.open(elt.href, '_blank');
          wnd.focus();
          return;
        }
        elt = elt.parentNode;
      }
    },
    onScroll(evt) {
      this.previewTop = evt.target.scrollTop < 10;
    },
  },
  mounted() {
    const previewElt = this.$el.querySelector('.preview__inner-2');
    const onDiscussionEvt = cb => (evt) => {
      let elt = evt.target;
      while (elt && elt !== previewElt) {
        if (elt.discussionId) {
          cb(elt.discussionId);
          return;
        }
        elt = elt.parentNode;
      }
    };

    const classToggler = toggle => (discussionId) => {
      previewElt.getElementsByClassName(`discussion-preview-highlighting--${discussionId}`)
        .cl_each(elt => elt.classList.toggle('discussion-preview-highlighting--hover', toggle));
      document.getElementsByClassName(`comment--discussion-${discussionId}`)
        .cl_each(elt => elt.classList.toggle('comment--hover', toggle));
    };

    previewElt.addEventListener('mouseover', onDiscussionEvt(classToggler(true)));
    previewElt.addEventListener('mouseout', onDiscussionEvt(classToggler(false)));
    previewElt.addEventListener('click', onDiscussionEvt((discussionId) => {
      store.commit('discussion/setCurrentDiscussionId', discussionId);
    }));

    this.$watch(
      () => store.state.discussion.currentDiscussionId,
      (discussionId, oldDiscussionId) => {
        if (oldDiscussionId) {
          previewElt.querySelectorAll(`.discussion-preview-highlighting--${oldDiscussionId}`)
            .cl_each(elt => elt.classList.remove('discussion-preview-highlighting--selected'));
        }
        if (discussionId) {
          previewElt.querySelectorAll(`.discussion-preview-highlighting--${discussionId}`)
            .cl_each(elt => elt.classList.add('discussion-preview-highlighting--selected'));
        }
      },
    );
  },
};
</script>

<style lang="scss">
@import '../styles/variables.scss';

.preview,
.preview__inner-1 {
  position: absolute;
  width: 100%;
  height: 100%;
}

.preview__inner-1 {
  overflow: auto;
}

.preview__inner-2 {
  margin: 0;
}

.preview__inner-2 > :first-child > :first-child {
  margin-top: 0;
}

$corner-size: 110px;

.preview__corner {
  position: absolute;
  top: 0;
  right: 0;

  &::before {
    content: '';
    position: absolute;
    right: 0;
    border-top: $corner-size solid rgba(0, 0, 0, 0.075);
    border-left: $corner-size solid transparent;
    pointer-events: none;

    .app--dark & {
      border-top-color: rgba(255, 255, 255, 0.075);
    }
  }
}

.preview__button {
  position: absolute;
  top: 15px;
  right: 15px;
  width: 40px;
  height: 40px;
  padding: 5px;
  color: rgba(0, 0, 0, 0.25);

  .app--dark & {
    color: rgba(255, 255, 255, 0.25);
  }

  &:active,
  &:focus,
  &:hover {
    color: rgba(0, 0, 0, 0.33);
    background-color: transparent;

    .app--dark & {
      color: rgba(255, 255, 255, 0.33);
    }
  }
}
</style>


================================================
FILE: src/components/SideBar.vue
================================================
<template>
  <div class="side-bar flex flex--column">
    <div class="side-title flex flex--row">
      <button v-if="panel !== 'menu'" class="side-title__button button" @click="setPanel('menu')" v-title="'Main menu'">
        <icon-dots-horizontal></icon-dots-horizontal>
      </button>
      <div class="side-title__title">
        {{panelName}}
      </div>
      <button class="side-title__button button" @click="toggleSideBar(false)" v-title="'Close side bar'">
        <icon-close></icon-close>
      </button>
    </div>
    <div class="side-bar__inner">
      <main-menu v-if="panel === 'menu'"></main-menu>
      <workspaces-menu v-else-if="panel === 'workspaces'"></workspaces-menu>
      <sync-menu v-else-if="panel === 'sync'"></sync-menu>
      <publish-menu v-else-if="panel === 'publish'"></publish-menu>
      <history-menu v-else-if="panel === 'history'"></history-menu>
      <export-menu v-else-if="panel === 'export'"></export-menu>
      <import-export-menu v-else-if="panel === 'importExport'"></import-export-menu>
      <workspace-backup-menu v-else-if="panel === 'workspaceBackups'"></workspace-backup-menu>
      <div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help">
        <pre class="markdown-highlighting" v-html="markdownSample"></pre>
      </div>
      <div class="side-bar__panel side-bar__panel--toc" :class="{'side-bar__panel--hidden': panel !== 'toc'}">
        <toc>
        </toc>
      </div>
    </div>
  </div>
</template>

<script>
import { mapActions } from 'vuex';
import Toc from './Toc';
import MainMenu from './menus/MainMenu';
import WorkspacesMenu from './menus/WorkspacesMenu';
import SyncMenu from './menus/SyncMenu';
import PublishMenu from './menus/PublishMenu';
import HistoryMenu from './menus/HistoryMenu';
import ImportExportMenu from './menus/ImportExportMenu';
import WorkspaceBackupMenu from './menus/WorkspaceBackupMenu';
import markdownSample from '../data/markdownSample.md';
import markdownConversionSvc from '../services/markdownConversionSvc';
import store from '../store';

const panelNames = {
  menu: 'Menu',
  workspaces: 'Workspaces',
  help: 'Markdown cheat sheet',
  toc: 'Table of contents',
  sync: 'Synchronize',
  publish: 'Publish',
  history: 'File history',
  importExport: 'Import/export',
  workspaceBackups: 'Workspace backups',
};

export default {
  components: {
    Toc,
    MainMenu,
    WorkspacesMenu,
    SyncMenu,
    PublishMenu,
    HistoryMenu,
    ImportExportMenu,
    WorkspaceBackupMenu,
  },
  data: () => ({
    markdownSample: markdownConversionSvc.highlight(markdownSample),
  }),
  computed: {
    panel() {
      if (store.state.light) {
        return null; // No menu in light mode
      }
      const result = store.getters['data/layoutSettings'].sideBarPanel;
      return panelNames[result] ? result : 'menu';
    },
    panelName() {
      return panelNames[this.panel];
    },
  },
  methods: {
    ...mapActions('data', [
      'toggleSideBar',
    ]),
    ...mapActions('data', {
      setPanel: 'setSideBarPanel',
    }),
  },
};
</script>

<style lang="scss">
@import '../styles/variables.scss';

.side-bar {
  overflow: hidden;
  height: 100%;

  hr {
    margin: 10px 40px;
    display: none;
    border-top: 1px solid $hr-color;
  }

  * + hr {
    display: block;
  }

  hr + hr {
    display: none;
  }

  .textfield {
    font-size: 14px;
    height: 26px;
  }
}

.side-bar__inner {
  position: relative;
  height: 100%;
}

.side-bar__panel {
  position: absolute;
  width: 100%;
  height: 100%;
  overflow: auto;

  &::after {
    content: '';
    display: block;
    height: 40px;
  }
}

.side-bar__panel--hidden {
  left: 1000px;
}

.side-bar__panel--menu {
  padding: 10px;
}

.side-bar__panel--help {
  padding: 0 10px 0 20px;

  pre {
    font-size: 0.9em;
    font-variant-ligatures: no-common-ligatures;
    line-height: 1.25;
    white-space: pre-wrap;
    word-break: break-word;
    word-wrap: break-word;
  }

  .code,
  .img,
  .imgref,
  .cl-toc {
    background-color: rgba(0, 0, 0, 0.05);
  }
}

.side-bar__info {
  padding: 10px;
  margin: -10px -10px 10px;
  background-color: $info-bg;
  font-size: 0.95em;

  p {
    margin: 10px 15px;
    font-size: 0.9rem;
    opacity: 0.67;
    line-height: 1.3;
  }
}
</style>


================================================
FILE: src/components/SplashScreen.vue
================================================
<template>
  <div class="splash-screen">
    <div class="splash-screen__inner logo-background"></div>
  </div>
</template>

<style lang="scss">
.splash-screen {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  padding: 25px;
}

.splash-screen__inner {
  margin: 0 auto;
  max-width: 600px;
  height: 100%;
}
</style>


================================================
FILE: src/components/StatusBar.vue
================================================
<template>
  <div class="stat-panel panel no-overflow">
    <div class="stat-panel__block stat-panel__block--left" v-if="styles.showEditor">
      <span class="stat-panel__block-name">
        Markdown
        <span v-if="textSelection">selection</span>
      </span>
      <span v-for="stat in textStats" :key="stat.id">
        <span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
      </span>
      <span class="stat-panel__value">Ln {{line}}, Col {{column}}</span>
    </div>
    <div class="stat-panel__block stat-panel__block--right">
      <span class="stat-panel__block-name">
        HTML
        <span v-if="htmlSelection">selection</span>
      </span>
      <span v-for="stat in htmlStats" :key="stat.id">
        <span class="stat-panel__value">{{stat.value}}</span> {{stat.name}}
      </span>
    </div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex';
import editorSvc from '../services/editorSvc';
import utils from '../services/utils';

class Stat {
  constructor(name, regex) {
    this.id = utils.uid();
    this.name = name;
    this.regex = new RegExp(regex, 'gm');
    this.value = null;
  }
}

export default {
  data: () => ({
    textSelection: false,
    htmlSelection: false,
    line: 0,
    column: 0,
    textStats: [
      new Stat('bytes', '[\\s\\S]'),
      new Stat('words', '\\S+'),
      new Stat('lines', '\n'),
    ],
    htmlStats: [
      new Stat('characters', '\\S'),
      new Stat('words', '\\S+'),
      new Stat('paragraphs', '\\S.*'),
    ],
  }),
  computed: mapGetters('layout', [
    'styles',
  ]),
  created() {
    editorSvc.$on('sectionList', () => this.computeText());
    editorSvc.$on('selectionRange', () => this.computeText());
    editorSvc.$on('previewCtx', () => this.computeHtml());
    editorSvc.$on('previewSelectionRange', () => this.computeHtml());
  },

  methods: {
    computeText() {
      setTimeout(() => {
        this.textSelection = false;
        let text = editorSvc.clEditor.getContent();
        const beforeText = text.slice(0, editorSvc.clEditor.selectionMgr.selectionEnd);
        const beforeLines = beforeText.split('\n');
        this.line = beforeLines.length;
        this.column = beforeLines.pop().length;

        const selectedText = editorSvc.clEditor.selectionMgr.getSelectedText();
        if (selectedText) {
          this.textSelection = true;
          text = selectedText;
        }
        this.textStats.forEach((stat) => {
          stat.value = (text.match(stat.regex) || []).length;
        });
      }, 10);
    },
    computeHtml() {
      setTimeout(() => {
        let text;
        if (editorSvc.previewSelectionRange) {
          text = `${editorSvc.previewSelectionRange}`;
        }
        this.htmlSelection = true;
        if (!text) {
          this.htmlSelection = false;
          ({ text } = editorSvc.previewCtx);
        }
        if (text != null) {
          this.htmlStats.forEach((stat) => {
            stat.value = (text.match(stat.regex) || []).length;
          });
        }
      }, 10);
    },
  },
};
</script>

<style lang="scss">
.stat-panel {
  position: absolute;
  width: 100%;
  height: 100%;
  color: #fff;
  font-size: 12px;
}

.stat-panel__block {
  margin: 0 10px;
}

.stat-panel__block--left {
  float: left;
}

.stat-panel__block--right {
  float: right;
}

.stat-panel__value {
  font-weight: 600;
  margin-left: 5px;
}
</style>


================================================
FILE: src/components/Toc.vue
================================================
<template>
  <div class="toc">
    <div class="toc__mask" :style="{top: (maskY - 5) + 'px'}"></div>
    <div class="toc__inner"></div>
  </div>
</template>

<script>
import { mapGetters } from 'vuex';
import editorSvc from '../services/editorSvc';

export default {
  data: () => ({
    maskY: 0,
  }),
  computed: {
    ...mapGetters('layout', [
      'styles',
    ]),
  },
  mounted() {
    const tocElt = this.$el.querySelector('.toc__inner');

    // TOC click behaviour
    let isMousedown;
    function onClick(e) {
      if (!isMousedown) {
        return;
      }
      e.preventDefault();
      const y = e.clientY - tocElt.getBoundingClientRect().top;

      editorSvc.previewCtx.sectionDescList.some((sectionDesc) => {
        if (y >= sectionDesc.tocDimension.endOffset) {
          return false;
        }
        const posInSection = (y - sectionDesc.tocDimension.startOffset)
          / (sectionDesc.tocDimension.height || 1);
        const editorScrollTop = sectionDesc.editorDimension.startOffset
          + (sectionDesc.editorDimension.height * posInSection);
        editorSvc.editorElt.parentNode.scrollTop = editorScrollTop;
        const previewScrollTop = sectionDesc.previewDimension.startOffset
          + (sectionDesc.previewDimension.height * posInSection);
        editorSvc.previewElt.parentNode.scrollTop = previewScrollTop;
        return true;
      });
    }

    tocElt.addEventListener('mouseup', () => {
      isMousedown = false;
    });
    tocElt.addEventListener('mouseleave', () => {
      isMousedown = false;
    });
    tocElt.addEventListener('mousedown', (e) => {
      isMousedown = e.which === 1;
      onClick(e);
    });
    tocElt.addEventListener('mousemove', (e) => {
      onClick(e);
    });

    // Change mask postion on scroll
    const updateMaskY = () => {
      const scrollPosition = editorSvc.getScrollPosition();
      if (scrollPosition) {
        const sectionDesc = editorSvc.previewCtxMeasured.sectionDescList[scrollPosition.sectionIdx];
        this.maskY = sectionDesc.tocDimension.startOffset +
          (scrollPosition.posInSection * sectionDesc.tocDimension.height);
      }
    };

    this.$nextTick(() => {
      editorSvc.editorElt.parentNode.addEventListener('scroll', () => {
        if (this.styles.showEditor) {
          updateMaskY();
        }
      });
      editorSvc.previewElt.parentNode.addEventListener('scroll', () => {
        if (!this.styles.showEditor) {
          updateMaskY();
        }
      });
    });
  },
};
</script>

<style lang="scss">
.toc__inner {
  position: relative;
  color: rgba(0, 0, 0, 0.67);
  cursor: pointer;
  font-size: 9px;
  padding: 10px 20px 40px;
  white-space: nowrap;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;

  * {
    font-weight: inherit;
    pointer-events: none;
  }

  .cl-toc-section {
    h1,
    h2 {
      &::after {
        display: none;
      }
    }

    h1 {
      margin: 1rem 0;
    }

    h2 {
      margin: 0.5rem 0;
      margin-left: 8px;
    }

    h3 {
      margin: 0.33rem 0;
      margin-left: 16px;
    }

    h4 {
      margin: 0.22rem 0;
      margin-left: 24px;
    }

    h5 {
      margin: 0.11rem 0;
      margin-left: 32px;
    }

    h6 {
      margin: 0;
      margin-left: 40px;
    }
  }
}

.toc__mask {
  position: absolute;
  left: 0;
  width: 100%;
  height: 35px;
  background-color: rgba(255, 255, 255, 0.2);
  pointer-events: none;
}
</style>


================================================
FILE: src/components/Tour.vue
================================================
<template>
  <div class="tour" @keydown.esc.stop="skip">
    <div class="tour-step" :class="'tour-step--' + step" :style="stepStyle">
      <div class="tour-step__inner" v-if="step === 'welcome'">
        <h2>Welcome back!</h2>
        <p>The new <b>StackEdit 5</b> is here!</p>
        <p>Please click <b>Next</b> to take a quick tour.</p>
        <div class="tour-step__button-bar">
          <button class="button" @click="finish">Skip</button>
          <button class="button button--resolve" @click="next">Next</button>
        </div>
      </div>
      <div class="tour-step__inner" v-else-if="step === 'editor'">
        <h2>Your Markdown editor</h2>
        <p>StackEdit converts your Markdown to HTML in real-time.</p>
        <p>Click <icon-side-preview></icon-side-preview> to toggle the side preview.</p>
        <div class="tour-step__button-bar">
          <button class="button" @click="finish">Skip</button>
          <button class="button button--resolve" @click="next">Next</button>
        </div>
      </div>
      <div class="tour-step__inner" v-else-if="step === 'explorer'">
        <h2>File explorer</h2>
        <p>StackEdit can manage multiple files and folders in a workspace.</p>
        <p>Click <icon-folder></icon-folder> to open the file explorer.</p>
        <div class="tour-step__button-bar">
          <button class="button" @click="finish">Skip</button>
          <button class="button button--resolve" @click="next">Next</button>
        </div>
      </div>
      <div class="tour-step__inner" v-else-if="step === 'menu'">
        <h2>Do a lot more!</h2>
        <p>StackEdit can also synchronize and publish your files, manage collaborative workspaces...</p>
        <p>Click <icon-provider provider-id="stackedit"></icon-provider> to explore the menu.</p>
        <div class="tour-step__button-bar">
          <button class="button" @click="finish">Skip</button>
          <button class="button button--resolve" @click="next">Next</button>
        </div>
      </div>
      <div class="tour-step__inner" v-else-if="step === 'end'">
        <h2>Enjoy!</h2>
        <p>If you like StackEdit, please rate 5 stars on the <a target="_blank" href="https://chrome.google.com/webstore/detail/iiooodelglhkcpgbajoejffhijaclcdg/reviews">Chrome Web Store</a>.</p>
        <p>You can also star the project on <a target="_blank" href="https://github.com/benweet/stackedit">GitHub</a> and join the <a target="_blank" href="https://community.stackedit.io/">community</a>.</p>
        <div class="tour-step__button-bar">
          <button class="button button--resolve" @click="finish">Ok</button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import Vue from 'vue';
import store from '../store';

const steps = [
  'welcome',
  'editor',
  'explorer',
  'menu',
  'end',
];

export default {
  data: () => ({
    stepIdx: 0,
    stepStyles: {},
  }),
  computed: {
    step() {
      return steps[this.stepIdx];
    },
    stepStyle() {
      return this.stepStyles[this.step] || {};
    },
  },
  methods: {
    updatePositions() {
      document.querySelectorAll('[tour-step-anchor]').cl_each((anchorElt) => {
        const anchorRect = anchorElt.getBoundingClientRect();
        const anchorSteps = (anchorElt.getAttribute('tour-step-anchor') || '').split(',');
        anchorSteps.forEach((step) => {
          const style = {
            top: `${anchorRect.top + (anchorRect.height / 2)}px`,
            left: `${anchorRect.left + (anchorRect.width / 2)}px`,
          };
          switch (step) {
            case 'welcome':
            case 'end': {
              style.top = `${anchorRect.top}px`;
              break;
            }
            case 'editor':
            case 'menu': {
              style.left = `${anchorRect.left}px`;
              break;
            }
            case 'explorer': {
              style.left = `${anchorRect.left + anchorRect.width}px`;
              break;
            }
            default:
              return;
          }
          Vue.set(this.stepStyles, step, style);
        });
      });
    },
    finish() {
      store.dispatch('data/patchLayoutSettings', {
        welcomeTourFinished: true,
      });
    },
    next() {
      this.stepIdx += 1;
    },
  },
  mounted() {
    this.$watch(
      () => store.getters['layout/styles'],
      () => this.updatePositions(),
      { immediate: true },
    );
  },
};
</script>


<style lang="scss">
@import '../styles/variables.scss';

.tour {
  position: absolute;
  top: 0;
  left: 0;
}

.tour-step {
  position: absolute;
}

$tour-step-background: transparentize(mix(#f3f3f3, $selection-highlighting-color, 75%), 0.025);
$tour-step-width: 240px;

.tour-step__inner {
  position: absolute;
  background-color: $tour-step-background;
  padding: 1.5em;
  font-size: 0.9em;
  line-height: 1.33;
  width: $tour-step-width;
  text-align: center;
  border-radius: $border-radius-base;

  h2 {
    margin: 0;

    &::after {
      display: none;
    }
  }

  .icon,
  .icon-provider {
    width: 1.25em;
    height: 1.25em;
    vertical-align: bottom;
    display: inline-block;
  }

  &::before {
    content: '';
    position: absolute;
  }

  .tour-step--welcome &,
  .tour-step--end & {
    left: -$tour-step-width/2;
    top: 36px;
    border-bottom-right-radius: 0;

    &::before {
      bottom: -10px;
      right: 0;
      border-top: 10px solid $tour-step-background;
      border-left: 10px solid transparent;
    }
  }

  .tour-step--editor &,
  .tour-step--menu & {
    right: 15px;
    border-top-right-radius: 0;

    &::before {
      top: 0;
      right: -10px;
      border-top: 10px solid $tour-step-background;
      border-right: 10px solid transparent;
    }
  }

  .tour-step--explorer & {
    left: 15px;
    border-top-left-radius: 0;

    &::before {
      top: 0;
      left: -10px;
      border-top: 10px solid $tour-step-background;
      border-left: 10px solid transparent;
    }
  }
}

.tour-step__button-bar {
  margin-top: 1.5em;
  display: flex;
  flex-direction: row;
  justify-content: flex-end;

  .button {
    font-size: 1.1em;
  }
}
</style>


================================================
FILE: src/components/UserImage.vue
================================================
<template>
  <div class="user-image" :style="{backgroundImage: url}">
  </div>
</template>

<script>
import userSvc from '../services/userSvc';
import store from '../store';

export default {
  props: ['userId'],
  computed: {
    sanitizedUserId() {
      return userSvc.sanitizeUserId(this.userId);
    },
    url() {
      const userInfo = store.state.userInfo.itemsById[this.sanitizedUserId];
      return userInfo && userInfo.imageUrl && `url('${userInfo.imageUrl}')`;
    },
  },
  watch: {
    sanitizedUserId: {
      handler: sanitizedUserId => userSvc.addUserId(sanitizedUserId),
      immediate: true,
    },
  },
};
</script>

<style lang="scss">
.user-image {
  width: 100%;
  height: 100%;
  background-color: #fff;
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
}
</style>


================================================
FILE: src/components/UserName.vue
================================================
<template>
  <span class="user-name">{{name}}</span>
</template>

<script>
import userSvc from '../services/userSvc';
import store from '../store';

export default {
  props: ['userId'],
  computed: {
    sanitizedUserId() {
      return userSvc.sanitizeUserId(this.userId);
    },
    name() {
      const userInfo = store.state.userInfo.itemsById[this.sanitizedUserId];
      return userInfo ? userInfo.name : 'Someone';
    },
  },
  watch: {
    sanitizedUserId: {
      handler: sanitizedUserId => userSvc.addUserId(sanitizedUserId),
      immediate: true,
    },
  },
};
</script>


================================================
FILE: src/components/common/EditorClassApplier.js
================================================
import cledit from '../../services/editor/cledit';
import editorSvc from '../../services/editorSvc';
import utils from '../../services/utils';

let savedSelection = null;
const nextTickCbs = [];
const nextTickExecCbs = cledit.Utils.debounce(() => {
  while (nextTickCbs.length) {
    nextTickCbs.shift()();
  }
  if (savedSelection) {
    editorSvc.clEditor.selectionMgr.setSelectionStartEnd(
      savedSelection.start,
      savedSelection.end,
    );
  }
  savedSelection = null;
});

const nextTick = (cb) => {
  nextTickCbs.push(cb);
  nextTickExecCbs();
};

const nextTickRestoreSelection = () => {
  savedSelection = {
    start: editorSvc.clEditor.selectionMgr.selectionStart,
    end: editorSvc.clEditor.selectionMgr.selectionEnd,
  };
  nextTickExecCbs();
};

export default class EditorClassApplier {
  constructor(classGetter, offsetGetter, properties) {
    this.classGetter = typeof classGetter === 'function' ? classGetter : () => classGetter;
    this.offsetGetter = typeof offsetGetter === 'function' ? offsetGetter : () => offsetGetter;
    this.properties = properties || {};
    this.eltCollection = editorSvc.editorElt.getElementsByClassName(this.classGetter()[0]);
    this.lastEltCount = this.eltCollection.length;

    this.restoreClass = () => {
      if (!this.eltCollection.length || this.eltCollection.length !== this.lastEltCount) {
        this.removeClass();
        this.applyClass();
      }
    };

    editorSvc.clEditor.on('contentChanged', this.restoreClass);
    nextTick(() => this.restoreClass());
  }

  applyClass() {
    if (!this.stopped) {
      const offset = this.offsetGetter();
      if (offset && offset.start !== offset.end) {
        const range = editorSvc.clEditor.selectionMgr.createRange(
          Math.min(offset.start, offset.end),
          Math.max(offset.start, offset.end),
        );
        const properties = {
          ...this.properties,
          className: this.classGetter().join(' '),
        };
        editorSvc.clEditor.watcher.noWatch(() => {
          utils.wrapRange(range, properties);
        });
        if (editorSvc.clEditor.selectionMgr.hasFocus()) {
          nextTickRestoreSelection();
        }
        this.lastEltCount = this.eltCollection.length;
      }
    }
  }

  removeClass() {
    editorSvc.clEditor.watcher.noWatch(() => {
      utils.unwrapRange(this.eltCollection);
    });
    if (editorSvc.clEditor.selectionMgr.hasFocus()) {
      nextTickRestoreSelection();
    }
  }

  stop() {
    editorSvc.clEditor.off('contentChanged', this.restoreClass);
    nextTick(() => this.removeClass());
    this.stopped = true;
  }
}


================================================
FILE: src/components/common/PreviewClassApplier.js
================================================
import cledit from '../../services/editor/cledit';
import editorSvc from '../../services/editorSvc';
import utils from '../../services/utils';

const nextTickCbs = [];
const nextTickExecCbs = cledit.Utils.debounce(() => {
  while (nextTickCbs.length) {
    nextTickCbs.shift()();
  }
});

const nextTick = (cb) => {
  nextTickCbs.push(cb);
  nextTickExecCbs();
};

export default class PreviewClassApplier {
  constructor(classGetter, offsetGetter, properties) {
    this.classGetter = typeof classGetter === 'function' ? classGetter : () => classGetter;
    this.offsetGetter = typeof offsetGetter === 'function' ? offsetGetter : () => offsetGetter;
    this.properties = properties || {};
    this.eltCollection = editorSvc.previewElt.getElementsByClassName(this.classGetter()[0]);
    this.lastEltCount = this.eltCollection.length;

    this.restoreClass = () => {
      if (!editorSvc.previewCtxWithDiffs) {
        this.removeClass();
      } else if (!this.eltCollection.length || this.eltCollection.length !== this.lastEltCount) {
        this.removeClass();
        this.applyClass();
      }
    };

    editorSvc.$on('previewCtxWithDiffs', this.restoreClass);
    nextTick(() => this.restoreClass());
  }

  applyClass() {
    if (!this.stopped) {
      const offset = this.offsetGetter();
      if (offset) {
        const offsetStart = editorSvc.getPreviewOffset(
          offset.start,
          editorSvc.previewCtx.sectionDescList,
        );
        const offsetEnd = editorSvc.getPreviewOffset(
          offset.end,
          editorSvc.previewCtx.sectionDescList,
        );
        if (offsetStart != null && offsetEnd != null && offsetStart !== offsetEnd) {
          const start = cledit.Utils.findContainer(
            editorSvc.previewElt,
            Math.min(offsetStart, offsetEnd),
          );
          const end = cledit.Utils.findContainer(
            editorSvc.previewElt,
            Math.max(offsetStart, offsetEnd),
          );
          const range = document.createRange();
          range.setStart(start.container, start.offsetInContainer);
          range.setEnd(end.container, end.offsetInContainer);
          const properties = {
            ...this.properties,
            className: this.classGetter().join(' '),
          };
          utils.wrapRange(range, properties);
          this.lastEltCount = this.eltCollection.length;
        }
      }
    }
  }

  removeClass() {
    utils.unwrapRange(this.eltCollection);
  }

  stop() {
    editorSvc.$off('previewCtxWithDiffs', this.restoreClass);
    nextTick(() => this.removeClass());
    this.stopped = true;
  }
}


================================================
FILE: src/components/common/vueGlobals.js
================================================
import Vue from 'vue';
import Clipboard from 'clipboard';
import timeSvc from '../../services/timeSvc';
import store from '../../store';

// Global directives
Vue.directive('focus', {
  inserted(el) {
    el.focus();
    const { value } = el;
    if (value && el.setSelectionRange) {
      el.setSelectionRange(0, value.length);
    }
  },
});

const setVisible = (el, value) => {
  el.style.display = value ? '' : 'none';
  if (value) {
    el.removeAttribute('aria-hidden');
  } else {
    el.setAttribute('aria-hidden', 'true');
  }
};
Vue.directive('show', {
  bind(el, { value }) {
    setVisible(el, value);
  },
  update(el, { value, oldValue }) {
    if (value !== oldValue) {
      setVisible(el, value);
    }
  },
});

const setElTitle = (el, title) => {
  el.title = title;
  el.setAttribute('aria-label', title);
};
Vue.directive('title', {
  bind(el, { value }) {
    setElTitle(el, value);
  },
  update(el, { value, oldValue }) {
    if (value !== oldValue) {
      setElTitle(el, value);
    }
  },
});

// Clipboard directive
const createClipboard = (el, value) => {
  el.seClipboard = new Clipboard(el, { text: () => value });
};
const destroyClipboard = (el) => {
  if (el.seClipboard) {
    el.seClipboard.destroy();
    el.seClipboard = null;
  }
};
Vue.directive('clipboard', {
  bind(el, { value }) {
    createClipboard(el, value);
  },
  update(el, { value, oldValue }) {
    if (value !== oldValue) {
      destroyClipboard(el);
      createClipboard(el, value);
    }
  },
  unbind(el) {
    destroyClipboard(el);
  },
});

// Global filters
Vue.filter('formatTime', time =>
  // Access the time counter for reactive refresh
  timeSvc.format(time, store.state.timeCounter));



================================================
FILE: src/components/gutters/Comment.vue
================================================
<template>
  <div class="comment">
    <div class="comment__header flex flex--row flex--space-between flex--align-center">
      <div class="comment__user flex flex--row flex--align-center">
        <div class="comment__user-image">
          <user-image :user-id="comment.sub"></user-image>
        </div>
        <button class="comment__remove-button button" v-title="'Remove comment'" @click="removeComment">
          <icon-delete></icon-delete>
        </button>
        <user-name :user-id="comment.sub"></user-name>
      </div>
      <div class="comment__created">{{comment.created | formatTime}}</div>
    </div>
    <div class="comment__text">
      <div class="comment__text-inner" v-html="text"></div>
    </div>
    <div class="comment__buttons flex flex--row flex--end" v-if="showReply">
      <button class="comment__button button" @click="setIsCommenting(true)">Reply</button>
    </div>
  </div>
</template>

<script>
import { mapMutations } from 'vuex';
import UserImage from '../UserImage';
import UserName from '../UserName';
import editorSvc from '../../services/editorSvc';
import htmlSanitizer from '../../libs/htmlSanitizer';
import store from '../../store';
import badgeSvc from '../../services/badgeSvc';

export default {
  components: {
    UserImage,
    UserName,
  },
  props: ['comment'],
  computed: {
    showReply() {
      return this.comment === store.getters['discussion/currentDiscussionLastComment'] &&
        !store.state.discussion.isCommenting;
    },
    text() {
      return htmlSanitizer.sanitizeHtml(editorSvc.converter.render(this.comment.text));
    },
  },
  methods: {
    ...mapMutations('discussion', [
      'setIsCommenting',
    ]),
    async removeComment() {
      try {
        await store.dispatch('modal/open', 'commentDeletion');
        store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment });
        badgeSvc.addBadge('removeComment');
      } catch (e) {
        // Cancel
      }
    },
  },
  mounted() {
    const isSticky = this.$el.parentNode.classList.contains('sticky-comment');
    if (isSticky) {
      const commentId = store.getters['discussion/currentDiscussionLastCommentId'];
      const scrollerElt = this.$el.querySelector('.comment__text-inner');

      let scrollerMirrorElt;
      const getScrollerMirrorElt = () => {
        if (!scrollerMirrorElt) {
          scrollerMirrorElt = document.querySelector(`.comment-list .comment--${commentId} .comment__text-inner`);
        }
        return scrollerMirrorElt || { scrollTop: 0 };
      };

      scrollerElt.scrollTop = getScrollerMirrorElt().scrollTop;
      scrollerElt.addEventListener('scroll', () => {
        getScrollerMirrorElt().scrollTop = scrollerElt.scrollTop;
      });
    }
  },
};
</script>


================================================
FILE: src/components/gutters/CommentList.vue
================================================
<template>
  <div class="comment-list" :class="stickyComment && 'comment-list--' + stickyComment" :style="{width: constants.gutterWidth + 'px'}">
    <comment v-for="(comment, discussionId) in currentFileDiscussionLastComments" :key="discussionId" v-if="comment.discussionId !== currentDiscussionId" :comment="comment" class="comment--last" :class="'comment--discussion-' + discussionId" :style="{top: tops[discussionId] + 'px'}" @click.native="setCurrentDiscussionId(discussionId)"></comment>
    <div class="comment-list__current-discussion" :style="{top: tops.current + 'px'}">
      <comment v-for="(comment, id) in currentDiscussionComments" :key="id" :comment="comment" :class="'comment--' + id"></comment>
      <new-comment v-if="isCommenting"></new-comment>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations } from 'vuex';
import Comment from './Comment';
import NewComment from './NewComment';
import editorSvc from '../../services/editorSvc';
import store from '../../store';
import utils from '../../services/utils';

export default {
  components: {
    Comment,
    NewComment,
  },
  data: () => ({
    tops: {},
  }),
  computed: {
    ...mapGetters('layout', [
      'constants',
      'styles',
    ]),
    ...mapState('discussion', [
      'currentDiscussionId',
      'isCommenting',
      'newCommentText',
      'stickyComment',
    ]),
    ...mapGetters('discussion', [
      'newDiscussion',
      'currentDiscussion',
      'currentFileDiscussions',
      'currentFileDiscussionLastComments',
      'currentDiscussionComments',
      'currentDiscussionLastCommentId',
    ]),
    updateTopsTrigger() {
      return utils.serializeObject([
        this.styles,
        this.currentFileDiscussionLastComments,
        this.currentDiscussionComments,
        this.currentDiscussionId,
        this.isCommenting,
      ]);
    },
    updateStickyTrigger() {
      return utils.serializeObject([
        this.updateTopsTrigger,
        this.newCommentText,
      ]);
    },
  },
  methods: {
    ...mapMutations('discussion', [
      'setCurrentDiscussionId',
    ]),
    updateTops() {
      const layoutSettings = store.getters['data/layoutSettings'];
      const minTop = -2;
      let minCommentTop = minTop;
      const getTop = (discussion, commentElt1, commentElt2, isCurrent) => {
        const firstElt = commentElt1 || commentElt2;
        const secondElt = commentElt1 && commentElt2;
        const coordinates = layoutSettings.showEditor
          ? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end)
          : editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end));
        let commentTop = minTop;
        if (coordinates) {
          commentTop = (coordinates.top + coordinates.height) - 80;
        }
        let top = commentTop;
        if (isCurrent) {
          top -= firstElt.offsetTop + 2; // 2 for top border
        }
        if (top < minTop) {
          commentTop += minTop - top;
          top = minTop;
        }
        if (commentTop < minCommentTop) {
          top += minCommentTop - commentTop;
          commentTop = minCommentTop;
        }
        minCommentTop = commentTop + firstElt.offsetHeight + 60;
        if (secondElt) {
          minCommentTop += secondElt.offsetHeight;
        }
        return top;
      };

      // Get the discussion top coordinates
      const tops = {};
      const discussions = this.currentFileDiscussions;
      Object.entries(discussions)
        .sort(([, discussion1], [, discussion2]) => discussion1.end - discussion2.end)
        .forEach(([discussionId, discussion]) => {
          if (discussion === this.currentDiscussion || discussion === this.newDiscussion) {
            tops.current = getTop(
              discussion,
              this.currentDiscussionLastCommentId
                && this.$el.querySelector(`.comment--${this.currentDiscussionLastCommentId}`),
              this.$el.querySelector('.comment--new'),
              true,
            );
          } else {
            tops[discussionId] = getTop(
              discussion,
              this.$el.querySelector(`.comment--discussion-${discussionId}`),
            );
          }
        });
      this.tops = tops;
    },
  },
  mounted() {
    this.$watch(
      () => this.updateTopsTrigger,
      () => this.updateTops(),
      { immediate: true },
    );

    const layoutSettings = store.getters['data/layoutSettings'];
    this.scrollerElt = layoutSettings.showEditor
      ? editorSvc.editorElt.parentNode
      : editorSvc.previewElt.parentNode;

    this.updateSticky = () => {
      let height = 0;
      let offsetTop = this.tops.current;
      const lastCommentElt = this.$el.querySelector(`.comment--${this.currentDiscussionLastCommentId}`);
      if (lastCommentElt) {
        height += lastCommentElt.clientHeight;
        offsetTop += lastCommentElt.offsetTop;
      }
      const newCommentElt = this.$el.querySelector('.comment--new');
      if (newCommentElt) {
        height += newCommentElt.clientHeight;
      }
      const currentDiscussionElt = document.querySelector('.current-discussion__inner');
      const minOffsetTop = this.scrollerElt.scrollTop + 10;
      const maxOffsetTop = (this.scrollerElt.scrollTop + this.scrollerElt.clientHeight) - height
        - currentDiscussionElt.clientHeight;
      let stickyComment = null;
      if (offsetTop > maxOffsetTop || maxOffsetTop < minOffsetTop) {
        stickyComment = 'bottom';
      } else if (offsetTop < minOffsetTop) {
        stickyComment = 'top';
      }
      if (store.state.discussion.stickyComment !== stickyComment) {
        store.commit('discussion/setStickyComment', stickyComment);
      }
    };

    this.scrollerElt.addEventListener('scroll', this.updateSticky);
    this.$watch(
      () => this.updateStickyTrigger,
      () => this.updateSticky(),
      { immediate: true },
    );

    // Move preview discussions once previewCtxWithDiffs has been calculated
    if (!editorSvc.previewCtxWithDiffs) {
      editorSvc.$once('previewCtxWithDiffs', () => {
        this.updateTops();
        this.updateSticky();
      });
    }
  },
  destroyed() {
    this.scrollerElt.removeEventListener('scroll', this.updateSticky);
  },
};
</script>

<style lang="scss">
@import '../../styles/variables.scss';

.comment-list {
  position: absolute;
  right: 0;
  font-size: 15px;
}

.comment--last,
.comment-list__current-discussion {
  position: absolute;
  width: 100%;
  padding-top: 10px;
}

/* use div selector to avoid collision with Prism */
div.comment {
  padding: 5px 10px 10px;
}

.comment--last {
  opacity: 0.33;
  cursor: pointer;

  * {
    pointer-events: none;
  }

  &:hover,
  &.comment--hover {
    opacity: 0.5;
  }
}

.comment__header {
  font-size: 0.75em;
  padding-bottom: 0.25em;
}

.comment__user-image {
  height: 20px;
  width: 20px;
  border-radius: $border-radius-base;
  overflow: hidden;
  margin-right: 5px;

  .comment:hover & {
    display: none;

    .sticky-comment & {
      display: block;
    }
  }

  .comment--new:hover &,
  .comment--last:hover & {
    display: block;
  }
}

.comment__remove-button {
  height: 20px;
  width: 20px;
  padding: 1px;
  color: rgba(0, 0, 0, 0.33);
  margin-right: 5px;
  display: none;

  &:active,
  &:focus,
  &:hover {
    color: rgba(0, 0, 0, 0.5);
  }

  .comment:hover & {
    display: block;

    .sticky-comment & {
      display: none;
    }
  }

  .comment--last:hover & {
    display: none;
  }
}

.comment__created {
  opacity: 0.5;
}

.comment__buttons {
  padding: 10px 5px 0;
}

.comment__button {
  padding: 0 8px;
  line-height: 28px;
  height: 28px;
}

.comment__text {
  position: relative;

  &::before {
    content: '';
    position: absolute;
    bottom: -8px;
    right: 0;
    border-top: 8px solid $editor-background-light;
    border-left: 8px solid transparent;

    .app--dark & {
      border-top-color: $editor-background-dark;
    }
  }

  h1,
  h2,
  h3,
  h4,
  h5,
  h6 {
    font-size: inherit;
  }

  h1,
  h2,
  h3,
  h4,
  h5,
  h6,
  p,
  blockquote,
  pre,
  ul,
  ol,
  dl {
    margin: 0.25em 0;
  }

  pre {
    font-variant-ligatures: no-common-ligatures;
    white-space: pre-wrap;
    word-break: break-word;
    word-wrap: break-word;
    caret-color: #000;
  }

  img {
    max-width: 100%;
  }

  .table-wrapper {
    max-width: 100%;
    overflow: auto;
  }
}

.comment__text-inner {
  min-height: 37px;
  max-height: 200px;
  overflow: auto;
  padding: 1px 8px;
  background-color: $editor-background-light;
  border: 1px solid transparent;
  border-radius: $border-radius-base;
  border-bottom-right-radius: 0;

  .app--dark & {
    background-color: $editor-background-dark;
  }

  .markdown-highlighting {
    padding: 5px 0;
    margin: 0;
  }
}
</style>


================================================
FILE: src/components/gutters/CurrentDiscussion.vue
================================================
<template>
  <div class="current-discussion" :style="{width: constants.gutterWidth + 'px'}">
    <sticky-comment v-if="stickyComment === 'bottom'"></sticky-comment>
    <div class="current-discussion__inner">
      <div class="flex flex--row flex--space-between">
        <div class="current-discussion__buttons flex flex--row flex--end">
          <button class="current-discussion__button button" v-if="showNext" @click="goToDiscussion(previousDiscussionId)" v-title="'Previous discussion'">
            <icon-arrow-left></icon-arrow-left>
          </button>
          <button class="current-discussion__button current-discussion__button--rotate button" v-if="showNext" @click="goToDiscussion(nextDiscussionId)" v-title="'Next discussion'">
            <icon-arrow-left></icon-arrow-left>
          </button>
        </div>
        <div class="current-discussion__buttons flex flex--row flex--end">
          <button class="current-discussion__button current-discussion__button--remove button" v-if="showRemove" @click="removeDiscussion" v-title="'Remove discussion'">
            <icon-delete></icon-delete>
          </button>
          <button class="current-discussion__button button" @click="setCurrentDiscussionId()" v-title="'Close discussion'">
            <icon-close></icon-close>
          </button>
        </div>
      </div>
      <div class="current-discussion__text markdown-highlighting markdown-highlighting--inline">
        <span @click="goToDiscussion()" v-html="text"></span>
      </div>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
import editorSvc from '../../services/editorSvc';
import animationSvc from '../../services/animationSvc';
import markdownConversionSvc from '../../services/markdownConversionSvc';
import StickyComment from './StickyComment';
import store from '../../store';
import badgeSvc from '../../services/badgeSvc';

export default {
  components: {
    StickyComment,
  },
  computed: {
    ...mapState('discussion', [
      'stickyComment',
      'currentDiscussionId',
    ]),
    ...mapGetters('discussion', [
      'currentDiscussion',
      'previousDiscussionId',
      'nextDiscussionId',
      'currentFileDiscussions',
      'currentDiscussionLastCommentId',
    ]),
    ...mapGetters('layout', [
      'constants',
    ]),
    text() {
      return markdownConversionSvc.highlight(this.currentDiscussion.text);
    },
    showNext() {
      return this.nextDiscussionId && this.nextDiscussionId !== this.currentDiscussionId;
    },
    showRemove() {
      return this.currentDiscussionLastCommentId;
    },
  },
  methods: {
    ...mapMutations('discussion', [
      'setCurrentDiscussionId',
    ]),
    ...mapActions('notification', [
      'info',
    ]),
    goToDiscussion(discussionId = this.currentDiscussionId) {
      this.setCurrentDiscussionId(discussionId);
      const layoutSettings = store.getters['data/layoutSettings'];
      const discussion = this.currentFileDiscussions[discussionId];
      const coordinates = layoutSettings.showEditor
        ? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end)
        : editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end));
      if (!coordinates) {
        this.info("Discussion can't be located in the file.");
      } else {
        const scrollerElt = layoutSettings.showEditor
          ? editorSvc.editorElt.parentNode
          : editorSvc.previewElt.parentNode;
        let scrollTop = coordinates.top - (scrollerElt.offsetHeight / 2);
        const maxScrollTop = scrollerElt.scrollHeight - scrollerElt.offsetHeight;
        if (scrollTop < 0) {
          scrollTop = 0;
        } else if (scrollTop > maxScrollTop) {
          scrollTop = maxScrollTop;
        }
        animationSvc.animate(scrollerElt)
          .scrollTop(scrollTop)
          .duration(200)
          .start();
      }
    },
    async removeDiscussion() {
      try {
        await store.dispatch('modal/open', 'discussionDeletion');
        store.dispatch('discussion/cleanCurrentFile', {
          filterDiscussion: this.currentDiscussion,
        });
        badgeSvc.addBadge('removeDiscussion');
      } catch (e) {
        // Cancel
      }
    },
  },
};
</script>

<style lang="scss">
@import '../../styles/variables.scss';

.current-discussion {
  position: absolute;
  right: 0;
  bottom: 0;

  .sticky-comment {
    position: relative;
  }
}

.current-discussion__inner {
  position: relative;
  font-size: 16px;
  background-color: $info-bg;
  max-height: 130px; /* 3 lines max */
  overflow: hidden;
}

.current-discussion__buttons {
  padding: 4px 4px 0;
}

.current-discussion__button {
  width: 30px;
  height: 28px;
  padding: 2px;
  flex: none;
  color: rgba(0, 0, 0, 0.5);

  &:active,
  &:focus,
  &:hover {
    color: rgba(0, 0, 0, 0.75);
  }
}

.current-discussion__button--remove {
  /* Make the trash a bit smaller */
  padding: 3px;
}

.current-discussion__button--rotate {
  transform: rotate(180deg);
}

.current-discussion__text {
  padding: 10px;

  span {
    padding: 0.2em 0;
    background-color: mix($editor-background-light, $selection-highlighting-color, 10%);
    cursor: pointer;

    .app--dark {
      background-color: mix($editor-background-dark, $selection-highlighting-color, 10%);
    }
  }
}
</style>


================================================
FILE: src/components/gutters/EditorNewDiscussionButton.vue
================================================
<template>
  <a class="new-discussion-button" href="javascript:void(0)" v-if="coordinates" :style="{top: coordinates.top + 'px'}" v-title="'Start a discussion'" @mousedown.stop.prevent @click="createNewDiscussion(selection)">
    <icon-message></icon-message>
  </a>
</template>

<script>
import { mapActions } from 'vuex';
import editorSvc from '../../services/editorSvc';
import store from '../../store';

export default {
  data: () => ({
    selection: null,
    coordinates: null,
  }),
  methods: {
    ...mapActions('discussion', [
      'createNewDiscussion',
    ]),
    checkSelection() {
      clearTimeout(this.timeout);
      this.timeout = setTimeout(() => {
        let offset;
        // Show the button if content is not a revision and has the focus
        if (
          !store.state.content.revisionContent &&
          editorSvc.clEditor.selectionMgr.hasFocus()
        ) {
          this.selection = editorSvc.getTrimmedSelection();
          if (this.selection) {
            const text = editorSvc.clEditor.getContent();
            offset = this.selection.end;
            while (offset && text[offset - 1] === '\n') {
              offset -= 1;
            }
          }
        }
        this.coordinates = offset
          ? editorSvc.clEditor.selectionMgr.getCoordinates(offset)
          : null;
      }, 25);
    },
  },
  mounted() {
    this.$nextTick(() => {
      editorSvc.clEditor.selectionMgr.on('selectionChanged', () => this.checkSelection());
      editorSvc.clEditor.selectionMgr.on('cursorCoordinatesChanged', () => this.checkSelection());
      editorSvc.clEditor.on('focus', () => this.checkSelection());
      editorSvc.clEditor.on('blur', () => this.checkSelection());
      this.checkSelection();
    });
  },
};
</script>


================================================
FILE: src/components/gutters/NewComment.vue
================================================
<template>
  <div class="comment comment--new" @keydown.esc.stop="cancelNewComment">
    <div class="comment__header flex flex--row flex--space-between flex--align-center">
      <div class="comment__user flex flex--row flex--align-center">
        <div class="comment__user-image">
          <user-image :user-id="userId"></user-image>
        </div>
        <span class="user-name">{{loginToken.name}}</span>
      </div>
    </div>
    <div class="comment__text">
      <div class="comment__text-inner">
        <pre class="markdown-highli
Download .txt
gitextract_i7x6xpbj/

├── .babelrc
├── .dockerignore
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .postcssrc.js
├── .stylelintrc
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── build/
│   ├── build.js
│   ├── check-versions.js
│   ├── deploy.sh
│   ├── dev-client.js
│   ├── dev-server.js
│   ├── utils.js
│   ├── vue-loader.conf.js
│   ├── webpack.base.conf.js
│   ├── webpack.dev.conf.js
│   ├── webpack.prod.conf.js
│   └── webpack.style.conf.js
├── chart/
│   ├── .helmignore
│   ├── Chart.yaml
│   ├── templates/
│   │   ├── NOTES.txt
│   │   ├── _helpers.tpl
│   │   ├── deployment.yaml
│   │   ├── ingress.yaml
│   │   ├── service.yaml
│   │   └── tests/
│   │       └── test-connection.yaml
│   └── values.yaml
├── chrome-app/
│   └── manifest.json
├── config/
│   ├── dev.env.js
│   ├── index.js
│   └── prod.env.js
├── gulpfile.js
├── index.html
├── index.js
├── package.json
├── server/
│   ├── conf.js
│   ├── github.js
│   ├── index.js
│   ├── pandoc.js
│   ├── pdf.js
│   └── user.js
├── src/
│   ├── components/
│   │   ├── App.vue
│   │   ├── ButtonBar.vue
│   │   ├── CodeEditor.vue
│   │   ├── ContextMenu.vue
│   │   ├── Editor.vue
│   │   ├── Explorer.vue
│   │   ├── ExplorerNode.vue
│   │   ├── FindReplace.vue
│   │   ├── Layout.vue
│   │   ├── Modal.vue
│   │   ├── NavigationBar.vue
│   │   ├── Notification.vue
│   │   ├── Preview.vue
│   │   ├── SideBar.vue
│   │   ├── SplashScreen.vue
│   │   ├── StatusBar.vue
│   │   ├── Toc.vue
│   │   ├── Tour.vue
│   │   ├── UserImage.vue
│   │   ├── UserName.vue
│   │   ├── common/
│   │   │   ├── EditorClassApplier.js
│   │   │   ├── PreviewClassApplier.js
│   │   │   └── vueGlobals.js
│   │   ├── gutters/
│   │   │   ├── Comment.vue
│   │   │   ├── CommentList.vue
│   │   │   ├── CurrentDiscussion.vue
│   │   │   ├── EditorNewDiscussionButton.vue
│   │   │   ├── NewComment.vue
│   │   │   ├── PreviewNewDiscussionButton.vue
│   │   │   └── StickyComment.vue
│   │   ├── menus/
│   │   │   ├── HistoryMenu.vue
│   │   │   ├── ImportExportMenu.vue
│   │   │   ├── MainMenu.vue
│   │   │   ├── PublishMenu.vue
│   │   │   ├── SyncMenu.vue
│   │   │   ├── WorkspaceBackupMenu.vue
│   │   │   ├── WorkspacesMenu.vue
│   │   │   └── common/
│   │   │       └── MenuEntry.vue
│   │   └── modals/
│   │       ├── AboutModal.vue
│   │       ├── AccountManagementModal.vue
│   │       ├── BadgeManagementModal.vue
│   │       ├── FilePropertiesModal.vue
│   │       ├── HtmlExportModal.vue
│   │       ├── ImageModal.vue
│   │       ├── LinkModal.vue
│   │       ├── PandocExportModal.vue
│   │       ├── PdfExportModal.vue
│   │       ├── PublishManagementModal.vue
│   │       ├── SettingsModal.vue
│   │       ├── SponsorModal.vue
│   │       ├── SyncManagementModal.vue
│   │       ├── TemplatesModal.vue
│   │       ├── WorkspaceManagementModal.vue
│   │       ├── common/
│   │       │   ├── FormEntry.vue
│   │       │   ├── ModalInner.vue
│   │       │   ├── Tab.vue
│   │       │   └── modalTemplate.js
│   │       └── providers/
│   │           ├── BloggerPagePublishModal.vue
│   │           ├── BloggerPublishModal.vue
│   │           ├── CouchdbCredentialsModal.vue
│   │           ├── CouchdbWorkspaceModal.vue
│   │           ├── DropboxAccountModal.vue
│   │           ├── DropboxPublishModal.vue
│   │           ├── DropboxSaveModal.vue
│   │           ├── GistPublishModal.vue
│   │           ├── GistSyncModal.vue
│   │           ├── GithubAccountModal.vue
│   │           ├── GithubOpenModal.vue
│   │           ├── GithubPublishModal.vue
│   │           ├── GithubSaveModal.vue
│   │           ├── GithubWorkspaceModal.vue
│   │           ├── GitlabAccountModal.vue
│   │           ├── GitlabOpenModal.vue
│   │           ├── GitlabPublishModal.vue
│   │           ├── GitlabSaveModal.vue
│   │           ├── GitlabWorkspaceModal.vue
│   │           ├── GoogleDriveAccountModal.vue
│   │           ├── GoogleDrivePublishModal.vue
│   │           ├── GoogleDriveSaveModal.vue
│   │           ├── GoogleDriveWorkspaceModal.vue
│   │           ├── GooglePhotoModal.vue
│   │           ├── WordpressPublishModal.vue
│   │           ├── ZendeskAccountModal.vue
│   │           └── ZendeskPublishModal.vue
│   ├── data/
│   │   ├── constants.js
│   │   ├── defaults/
│   │   │   ├── defaultLayoutSettings.js
│   │   │   ├── defaultLocalSettings.js
│   │   │   ├── defaultSettings.yml
│   │   │   └── defaultWorkspaces.js
│   │   ├── empties/
│   │   │   ├── emptyContent.js
│   │   │   ├── emptyContentState.js
│   │   │   ├── emptyFile.js
│   │   │   ├── emptyFolder.js
│   │   │   ├── emptyPublishLocation.js
│   │   │   ├── emptySyncLocation.js
│   │   │   ├── emptySyncedContent.js
│   │   │   ├── emptyTemplateHelpers.js
│   │   │   └── emptyTemplateValue.html
│   │   ├── faq.md
│   │   ├── features.js
│   │   ├── markdownSample.md
│   │   ├── pagedownButtons.js
│   │   ├── presets.js
│   │   ├── simpleModals.js
│   │   ├── templates/
│   │   │   ├── jekyllSiteTemplate.html
│   │   │   ├── plainHtmlTemplate.html
│   │   │   ├── styledHtmlTemplate.html
│   │   │   └── styledHtmlWithTocTemplate.html
│   │   └── welcomeFile.md
│   ├── extensions/
│   │   ├── abcExtension.js
│   │   ├── emojiExtension.js
│   │   ├── index.js
│   │   ├── katexExtension.js
│   │   ├── libs/
│   │   │   ├── markdownItAnchor.js
│   │   │   ├── markdownItMath.js
│   │   │   └── markdownItTasklist.js
│   │   ├── markdownExtension.js
│   │   └── mermaidExtension.js
│   ├── icons/
│   │   ├── Alert.vue
│   │   ├── ArrowLeft.vue
│   │   ├── CheckCircle.vue
│   │   ├── Close.vue
│   │   ├── CodeBraces.vue
│   │   ├── CodeTags.vue
│   │   ├── ContentCopy.vue
│   │   ├── ContentSave.vue
│   │   ├── Database.vue
│   │   ├── Delete.vue
│   │   ├── DotsHorizontal.vue
│   │   ├── Download.vue
│   │   ├── Eye.vue
│   │   ├── FileImage.vue
│   │   ├── FileMultiple.vue
│   │   ├── FilePlus.vue
│   │   ├── Folder.vue
│   │   ├── FolderMultiple.vue
│   │   ├── FolderPlus.vue
│   │   ├── FormatBold.vue
│   │   ├── FormatItalic.vue
│   │   ├── FormatListBulleted.vue
│   │   ├── FormatListChecks.vue
│   │   ├── FormatListNumbers.vue
│   │   ├── FormatQuoteClose.vue
│   │   ├── FormatSize.vue
│   │   ├── FormatStrikethrough.vue
│   │   ├── HelpCircle.vue
│   │   ├── History.vue
│   │   ├── Information.vue
│   │   ├── Key.vue
│   │   ├── LinkVariant.vue
│   │   ├── Login.vue
│   │   ├── Logout.vue
│   │   ├── Magnify.vue
│   │   ├── Menu.vue
│   │   ├── Message.vue
│   │   ├── NavigationBar.vue
│   │   ├── OpenInNew.vue
│   │   ├── Pen.vue
│   │   ├── Printer.vue
│   │   ├── Provider.vue
│   │   ├── Redo.vue
│   │   ├── ScrollSync.vue
│   │   ├── Seal.vue
│   │   ├── Settings.vue
│   │   ├── SidePreview.vue
│   │   ├── SignalOff.vue
│   │   ├── StatusBar.vue
│   │   ├── Sync.vue
│   │   ├── SyncOff.vue
│   │   ├── Table.vue
│   │   ├── Target.vue
│   │   ├── Toc.vue
│   │   ├── Undo.vue
│   │   ├── Upload.vue
│   │   ├── ViewList.vue
│   │   └── index.js
│   ├── index.js
│   ├── libs/
│   │   ├── clunderscore.js
│   │   ├── htmlSanitizer.js
│   │   └── pagedown.js
│   ├── services/
│   │   ├── animationSvc.js
│   │   ├── backupSvc.js
│   │   ├── badgeSvc.js
│   │   ├── diffUtils.js
│   │   ├── editor/
│   │   │   ├── cledit/
│   │   │   │   ├── cleditCore.js
│   │   │   │   ├── cleditHighlighter.js
│   │   │   │   ├── cleditKeystroke.js
│   │   │   │   ├── cleditMarker.js
│   │   │   │   ├── cleditSelectionMgr.js
│   │   │   │   ├── cleditUndoMgr.js
│   │   │   │   ├── cleditUtils.js
│   │   │   │   ├── cleditWatcher.js
│   │   │   │   └── index.js
│   │   │   ├── editorSvcDiscussions.js
│   │   │   ├── editorSvcUtils.js
│   │   │   └── sectionUtils.js
│   │   ├── editorSvc.js
│   │   ├── explorerSvc.js
│   │   ├── exportSvc.js
│   │   ├── extensionSvc.js
│   │   ├── gitWorkspaceSvc.js
│   │   ├── localDbSvc.js
│   │   ├── markdownConversionSvc.js
│   │   ├── markdownGrammarSvc.js
│   │   ├── networkSvc.js
│   │   ├── optional/
│   │   │   ├── index.js
│   │   │   ├── keystrokes.js
│   │   │   ├── scrollSync.js
│   │   │   ├── shortcuts.js
│   │   │   └── taskChange.js
│   │   ├── providers/
│   │   │   ├── bloggerPageProvider.js
│   │   │   ├── bloggerProvider.js
│   │   │   ├── common/
│   │   │   │   ├── Provider.js
│   │   │   │   └── providerRegistry.js
│   │   │   ├── couchdbWorkspaceProvider.js
│   │   │   ├── dropboxProvider.js
│   │   │   ├── gistProvider.js
│   │   │   ├── githubProvider.js
│   │   │   ├── githubWorkspaceProvider.js
│   │   │   ├── gitlabProvider.js
│   │   │   ├── gitlabWorkspaceProvider.js
│   │   │   ├── googleDriveAppDataProvider.js
│   │   │   ├── googleDriveProvider.js
│   │   │   ├── googleDriveWorkspaceProvider.js
│   │   │   ├── helpers/
│   │   │   │   ├── couchdbHelper.js
│   │   │   │   ├── dropboxHelper.js
│   │   │   │   ├── githubHelper.js
│   │   │   │   ├── gitlabHelper.js
│   │   │   │   ├── googleHelper.js
│   │   │   │   ├── wordpressHelper.js
│   │   │   │   └── zendeskHelper.js
│   │   │   ├── wordpressProvider.js
│   │   │   └── zendeskProvider.js
│   │   ├── publishSvc.js
│   │   ├── syncSvc.js
│   │   ├── tempFileSvc.js
│   │   ├── templateWorker.js
│   │   ├── timeSvc.js
│   │   ├── userSvc.js
│   │   ├── utils.js
│   │   └── workspaceSvc.js
│   ├── store/
│   │   ├── content.js
│   │   ├── contentState.js
│   │   ├── contextMenu.js
│   │   ├── data.js
│   │   ├── discussion.js
│   │   ├── explorer.js
│   │   ├── file.js
│   │   ├── findReplace.js
│   │   ├── folder.js
│   │   ├── index.js
│   │   ├── layout.js
│   │   ├── locationTemplate.js
│   │   ├── modal.js
│   │   ├── moduleTemplate.js
│   │   ├── notification.js
│   │   ├── queue.js
│   │   ├── syncedContent.js
│   │   ├── userInfo.js
│   │   └── workspace.js
│   └── styles/
│       ├── app.scss
│       ├── base.scss
│       ├── fonts.scss
│       ├── index.js
│       ├── markdownHighlighting.scss
│       ├── prism.scss
│       └── variables.scss
├── static/
│   ├── landing/
│   │   └── index.html
│   ├── oauth2/
│   │   └── callback.html
│   └── sitemap.xml
└── test/
    └── unit/
        ├── .eslintrc
        ├── jest.conf.js
        ├── mocks/
        │   ├── cryptoMock.js
        │   ├── localStorageMock.js
        │   ├── mutationObserverMock.js
        │   └── templateWorkerMock.js
        ├── setup.js
        └── specs/
            ├── components/
            │   ├── ButtonBar.spec.js
            │   ├── ContextMenu.spec.js
            │   ├── Explorer.spec.js
            │   ├── ExplorerNode.spec.js
            │   ├── NavigationBar.spec.js
            │   └── Notification.spec.js
            └── specUtils.js
Download .txt
SYMBOL INDEX (522 symbols across 93 files)

FILE: build/check-versions.js
  function exec (line 5) | function exec (cmd) {

FILE: build/utils.js
  function generateLoaders (line 24) | function generateLoaders (loader, loaderOptions) {

FILE: build/webpack.base.conf.js
  function resolve (line 9) | function resolve (dir) {

FILE: build/webpack.prod.conf.js
  function resolve (line 15) | function resolve (dir) {

FILE: build/webpack.style.conf.js
  function resolve (line 11) | function resolve (dir) {

FILE: server/github.js
  function githubToken (line 5) | function githubToken(clientId, code) {

FILE: server/pandoc.js
  function onError (line 88) | function onError(error) {

FILE: server/pdf.js
  function waitForJavaScript (line 9) | function waitForJavaScript() {
  function onError (line 75) | function onError(err) {

FILE: server/user.js
  constant AWS (line 2) | const AWS = require('aws-sdk');

FILE: src/components/common/EditorClassApplier.js
  class EditorClassApplier (line 33) | class EditorClassApplier {
    method constructor (line 34) | constructor(classGetter, offsetGetter, properties) {
    method applyClass (line 52) | applyClass() {
    method removeClass (line 75) | removeClass() {
    method stop (line 84) | stop() {

FILE: src/components/common/PreviewClassApplier.js
  class PreviewClassApplier (line 17) | class PreviewClassApplier {
    method constructor (line 18) | constructor(classGetter, offsetGetter, properties) {
    method applyClass (line 38) | applyClass() {
    method removeClass (line 73) | removeClass() {
    method stop (line 77) | stop() {

FILE: src/components/common/vueGlobals.js
  method inserted (line 8) | inserted(el) {
  method bind (line 26) | bind(el, { value }) {
  method update (line 29) | update(el, { value, oldValue }) {
  method bind (line 41) | bind(el, { value }) {
  method update (line 44) | update(el, { value, oldValue }) {
  method bind (line 62) | bind(el, { value }) {
  method update (line 65) | update(el, { value, oldValue }) {
  method unbind (line 71) | unbind(el) {

FILE: src/components/modals/common/modalTemplate.js
  method config (line 21) | config() {
  method currentFileName (line 24) | currentFileName() {
  method setError (line 31) | setError(name) {
  method get (line 45) | get() {
  method set (line 48) | set(value) {

FILE: src/data/features.js
  class Feature (line 1) | class Feature {
    method constructor (line 2) | constructor(id, badgeName, description, children = null) {
    method toBadge (line 9) | toBadge(badgeCreations) {

FILE: src/extensions/libs/markdownItMath.js
  function texMath (line 1) | function texMath(state, silent) {

FILE: src/extensions/libs/markdownItTasklist.js
  function attrSet (line 1) | function attrSet(token, name, value) {

FILE: src/libs/clunderscore.js
  function build (line 140) | function build(properties) {

FILE: src/libs/htmlSanitizer.js
  function sanitizeUri (line 6) | function sanitizeUri(uri, isImage) {
  function makeMap (line 127) | function makeMap(str, lowercaseKeys) {
  function htmlParser (line 150) | function htmlParser(html, handler) {
  function decodeEntities (line 315) | function decodeEntities(value) {
  function encodeEntities (line 333) | function encodeEntities(value) {
  function htmlSanitizeWriter (line 358) | function htmlSanitizeWriter(buf, uriValidator) {
  function sanitizeHtml (line 412) | function sanitizeHtml(html) {

FILE: src/libs/pagedown.js
  function Pagedown (line 64) | function Pagedown(options) {
  function Chunks (line 149) | function Chunks() { }
  function TextareaState (line 327) | function TextareaState(input) {
  function UIManager (line 390) | function UIManager(input, commandManager) {
  function CommandManager (line 492) | function CommandManager(pluginHooks, getString) {
  function properlyEncoded (line 692) | function properlyEncoded(linkdef) {
  function padding (line 1179) | function padding(len, str) {
  function stripTailPipes (line 1189) | function stripTailPipes(str) {
  function splitCells (line 1193) | function splitCells(str) {
  function addTailPipes (line 1197) | function addTailPipes(str) {
  function joinCells (line 1205) | function joinCells(arr) {
  function formatTable (line 1209) | function formatTable(text, appendNewline) {

FILE: src/services/animationSvc.js
  function getStyle (line 32) | function getStyle(styles) {
  function identity (line 46) | function identity(x) {
  function ElementAttribute (line 50) | function ElementAttribute(name) {
  function StyleAttribute (line 62) | function StyleAttribute(name, unit, defaultValue, wrap = identity) {
  function TransformAttribute (line 77) | function TransformAttribute(name, unit, defaultValue, wrap = identity) {
  class Animation (line 114) | class Animation {
    method constructor (line 115) | constructor(elt) {
    method start (line 121) | start(param1, param2, param3) {
    method stop (line 152) | stop() {
    method loop (line 156) | loop(useTransition) {
  function animate (line 228) | function animate(elt) {

FILE: src/services/backupSvc.js
  method importBackup (line 5) | async importBackup(jsonValue) {

FILE: src/services/badgeSvc.js
  method addBadge (line 18) | addBadge(featureId) {

FILE: src/services/diffUtils.js
  function makePatchableText (line 7) | function makePatchableText(content, markerKeys, markerIdxMap) {
  function stripDiscussionOffsets (line 53) | function stripDiscussionOffsets(objectMap) {
  function restoreDiscussionOffsets (line 66) | function restoreDiscussionOffsets(content, markerKeys) {
  function mergeText (line 96) | function mergeText(serverText, clientText, lastMergedText) {
  function mergeValues (line 119) | function mergeValues(serverValue, clientValue, lastMergedValue) {
  function mergeObjects (line 141) | function mergeObjects(serverObject, clientObject, lastMergedObject = {}) {
  function mergeContent (line 155) | function mergeContent(serverContent, clientContent, lastMergedContent = ...

FILE: src/services/editor/cledit/cleditCore.js
  function cledit (line 6) | function cledit(contentElt, scrollEltOpt, isMarkdown = false) {

FILE: src/services/editor/cledit/cleditHighlighter.js
  function createStyleSheet (line 5) | function createStyleSheet(document) {
  function Highlighter (line 13) | function Highlighter(editor) {

FILE: src/services/editor/cledit/cleditKeystroke.js
  function Keystroke (line 3) | function Keystroke(handler, priority) {
  function getNextWordOffset (line 21) | function getNextWordOffset(text, offset, isBackward) {

FILE: src/services/editor/cledit/cleditMarker.js
  constant DIFF_DELETE (line 3) | const DIFF_DELETE = -1;
  constant DIFF_INSERT (line 4) | const DIFF_INSERT = 1;
  constant DIFF_EQUAL (line 5) | const DIFF_EQUAL = 0;
  class Marker (line 9) | class Marker {
    method constructor (line 10) | constructor(offset, trailing) {
    method adjustOffset (line 17) | adjustOffset(diffs) {

FILE: src/services/editor/cledit/cleditSelectionMgr.js
  function SelectionMgr (line 3) | function SelectionMgr(editor) {

FILE: src/services/editor/cledit/cleditUndoMgr.js
  function UndoMgr (line 4) | function UndoMgr(editor) {

FILE: src/services/editor/cledit/cleditUtils.js
  function flush (line 14) | function flush() {

FILE: src/services/editor/cledit/cleditWatcher.js
  function Watcher (line 3) | function Watcher(editor, listener) {

FILE: src/services/editor/editorSvcDiscussions.js
  function getDiscussionMarkers (line 21) | function getDiscussionMarkers(discussion, discussionId, onMarker) {
  function syncDiscussionMarkers (line 38) | function syncDiscussionMarkers(content, writeOffsets) {
  function removeDiscussionMarkers (line 75) | function removeDiscussionMarkers() {
  function makePatches (line 86) | function makePatches() {
  function applyPatches (line 91) | function applyPatches(patches) {
  function reversePatches (line 107) | function reversePatches(patches) {
  method createClEditor (line 118) | createClEditor(editorElt) {
  method initClEditorInternal (line 142) | initClEditorInternal(opts) {
  method applyContent (line 168) | applyContent() {
  method getTrimmedSelection (line 179) | getTrimmedSelection() {
  method initHighlighters (line 192) | initHighlighters() {

FILE: src/services/editor/editorSvcUtils.js
  method getScrollPosition (line 12) | getScrollPosition(elt = store.getters['layout/styles'].showEditor
  method restoreScrollPosition (line 39) | restoreScrollPosition() {
  method getPreviewOffset (line 57) | getPreviewOffset(
  method getEditorOffset (line 85) | getEditorOffset(
  method getPreviewOffsetCoordinates (line 115) | getPreviewOffsetCoordinates(offset) {
  method scrollToAnchor (line 133) | scrollToAnchor(anchor) {

FILE: src/services/editor/sectionUtils.js
  class SectionDimension (line 1) | class SectionDimension {
    method constructor (line 2) | constructor(startOffset, endOffset) {
  method measureSectionDimensions (line 44) | measureSectionDimensions(editorSvc) {

FILE: src/services/editorSvc.js
  class SectionDesc (line 33) | class SectionDesc {
    method constructor (line 34) | constructor(section, previewElt, tocElt, html) {
  method initPrism (line 70) | initPrism() {
  method initConverter (line 81) | initConverter() {
  method initClEditor (line 88) | initClEditor() {
  method convert (line 114) | convert() {
  method refreshPreview (line 123) | async refreshPreview() {
  method makeTextToPreviewDiffs (line 249) | makeTextToPreviewDiffs() {
  method getPandocAst (line 338) | getPandocAst() {
  method init (line 345) | init(editorElt, previewElt, tocElt) {

FILE: src/services/explorerSvc.js
  method newItem (line 6) | newItem(isFolder = false) {
  method deleteItem (line 19) | async deleteItem() {

FILE: src/services/exportSvc.js
  function groupHeadings (line 10) | function groupHeadings(headings, level = 1) {
  method applyTemplate (line 45) | async applyTemplate(fileId, template = {
  method exportToDisk (line 116) | async exportToDisk(fileId, type, template) {

FILE: src/services/extensionSvc.js
  method onGetOptions (line 6) | onGetOptions(listener) {
  method onInitConverter (line 10) | onInitConverter(priority, listener) {
  method onSectionPreview (line 14) | onSectionPreview(listener) {
  method getOptions (line 18) | getOptions(properties, isCurrentFile) {
  method initConverter (line 25) | initConverter(markdown, options) {
  method sectionPreview (line 32) | sectionPreview(elt, options, isEditor) {

FILE: src/services/gitWorkspaceSvc.js
  method makeChanges (line 8) | makeChanges(tree) {

FILE: src/services/localDbSvc.js
  class Connection (line 16) | class Connection {
    method constructor (line 17) | constructor(workspaceId = store.getters['workspace/currentWorkspace']....
    method createTx (line 64) | createTx(onTx, onError) {
  method syncLocalStorage (line 98) | syncLocalStorage() {
  method sync (line 131) | async sync() {
  method readAll (line 158) | readAll(tx, cb) {
  method writeAll (line 200) | writeAll(storeItemMap, tx) {
  method readDbItem (line 253) | readDbItem(dbItem, storeItemMap) {
  method loadItem (line 279) | async loadItem(id) {
  method unloadContents (line 311) | async unloadContents() {
  method init (line 329) | async init() {
  method getWorkspaceItems (line 432) | getWorkspaceItems(workspaceId, onItem, onFinish = () => {}) {

FILE: src/services/markdownConversionSvc.js
  function createFlagMap (line 63) | function createFlagMap(arr) {
  function hashArray (line 93) | function hashArray(arr, valueHash, valueArray) {
  method init (line 112) | init() {
  method createConverter (line 129) | createConverter(options) {
  method parseSections (line 155) | parseSections(converter, text) {
  method convert (line 219) | convert(parsingCtx, previousConversionCtx) {
  method highlight (line 268) | highlight(markdown, converter = this.defaultConverter, grammars = this.d...

FILE: src/services/markdownGrammarSvc.js
  method makeGrammars (line 49) | makeGrammars(options) {

FILE: src/services/networkSvc.js
  function parseHeaders (line 16) | function parseHeaders(xhr) {
  function isRetriable (line 28) | function isRetriable(err) {
  method init (line 37) | async init() {
  method getServerConf (line 109) | async getServerConf() {
  method isWindowFocused (line 121) | isWindowFocused() {
  method isUserActive (line 126) | isUserActive() {
  method isConfLoaded (line 129) | isConfLoaded() {
  method loadScript (line 132) | async loadScript(url) {
  method startOauth2 (line 147) | async startOauth2(url, params = {}, silent = false, reattempt = false) {
  method request (line 241) | async request(config, offlineCheck = false) {

FILE: src/services/optional/keystrokes.js
  function fixNumberedList (line 10) | function fixNumberedList(state, indent) {
  function enterKeyHandler (line 100) | function enterKeyHandler(evt, state) {
  function tabKeyHandler (line 136) | function tabKeyHandler(evt, state) {

FILE: src/services/optional/scrollSync.js
  function throttle (line 19) | function throttle(func, wait) {

FILE: src/services/optional/shortcuts.js
  method sync (line 36) | sync() {
  method expand (line 44) | expand(param1, param2) {

FILE: src/services/providers/bloggerPageProvider.js
  method getToken (line 8) | getToken({ sub }) {
  method getLocationUrl (line 12) | getLocationUrl({ blogId, pageId }) {
  method getLocationDescription (line 15) | getLocationDescription({ pageId }) {
  method publish (line 18) | async publish(token, html, metadata, publishLocation) {
  method makeLocation (line 34) | makeLocation(token, blogUrl, pageId) {

FILE: src/services/providers/bloggerProvider.js
  method getToken (line 8) | getToken({ sub }) {
  method getLocationUrl (line 12) | getLocationUrl({ blogId, postId }) {
  method getLocationDescription (line 15) | getLocationDescription({ postId }) {
  method publish (line 18) | async publish(token, html, metadata, publishLocation) {
  method makeLocation (line 34) | makeLocation(token, blogUrl, postId) {

FILE: src/services/providers/common/Provider.js
  class Provider (line 9) | class Provider {
    method constructor (line 13) | constructor(props) {
    method serializeContent (line 21) | static serializeContent(content) {
    method parseContent (line 46) | static parseContent(serializedContent, id) {
    method openFileWithLocation (line 83) | static openFileWithLocation(criteria) {

FILE: src/services/providers/common/providerRegistry.js
  method register (line 3) | register(provider) {

FILE: src/services/providers/couchdbWorkspaceProvider.js
  method getToken (line 12) | getToken() {
  method getWorkspaceParams (line 15) | getWorkspaceParams({ dbUrl }) {
  method getWorkspaceLocationUrl (line 21) | getWorkspaceLocationUrl({ dbUrl }) {
  method getSyncDataUrl (line 24) | getSyncDataUrl(fileSyncData, { id }) {
  method getSyncDataDescription (line 28) | getSyncDataDescription(fileSyncData, { id }) {
  method initWorkspace (line 31) | async initWorkspace() {
  method getChanges (line 65) | async getChanges() {
  method onChangesApplied (line 90) | onChangesApplied() {
  method saveWorkspaceItem (line 95) | async saveWorkspaceItem({ item, syncData }) {
  method removeWorkspaceItem (line 115) | removeWorkspaceItem({ syncData }) {
  method downloadWorkspaceContent (line 119) | async downloadWorkspaceContent({ token, contentSyncData }) {
  method downloadWorkspaceData (line 132) | async downloadWorkspaceData({ token, syncData }) {
  method uploadWorkspaceContent (line 149) | async uploadWorkspaceContent({ token, content, contentSyncData }) {
  method uploadWorkspaceData (line 174) | async uploadWorkspaceData({ token, item, syncData }) {
  method listFileRevisions (line 199) | async listFileRevisions({ token, contentSyncDataId }) {
  method loadFileRevision (line 214) | async loadFileRevision({ token, contentSyncDataId, revision }) {
  method getFileRevisionContent (line 224) | async getFileRevisionContent({ token, contentSyncDataId, revisionId }) {

FILE: src/services/providers/dropboxProvider.js
  method getToken (line 23) | getToken({ sub }) {
  method getLocationUrl (line 26) | getLocationUrl({ path }) {
  method getLocationDescription (line 31) | getLocationDescription({ path, dropboxFileId }) {
  method checkPath (line 34) | checkPath(path) {
  method downloadContent (line 37) | async downloadContent(token, syncLocation) {
  method uploadContent (line 45) | async uploadContent(token, content, syncLocation) {
  method publish (line 58) | async publish(token, html, metadata, publishLocation) {
  method openFiles (line 71) | async openFiles(token, paths) {
  method makeLocation (line 119) | makeLocation(token, path) {
  method listFileRevisions (line 126) | async listFileRevisions({ token, syncLocation }) {
  method loadFileRevision (line 138) | async loadFileRevision() {
  method getFileRevisionContent (line 142) | async getFileRevisionContent({

FILE: src/services/providers/gistProvider.js
  method getToken (line 10) | getToken({ sub }) {
  method getLocationUrl (line 13) | getLocationUrl({ gistId }) {
  method getLocationDescription (line 16) | getLocationDescription({ filename }) {
  method downloadContent (line 19) | async downloadContent(token, syncLocation) {
  method uploadContent (line 26) | async uploadContent(token, content, syncLocation) {
  method publish (line 40) | async publish(token, html, metadata, publishLocation) {
  method makeLocation (line 52) | makeLocation(token, filename, isPublic, gistId) {
  method listFileRevisions (line 61) | async listFileRevisions({ token, syncLocation }) {
  method loadFileRevision (line 77) | async loadFileRevision() {
  method getFileRevisionContent (line 81) | async getFileRevisionContent({

FILE: src/services/providers/githubProvider.js
  method getToken (line 13) | getToken({ sub }) {
  method getLocationUrl (line 16) | getLocationUrl({
  method getLocationDescription (line 24) | getLocationDescription({ path }) {
  method downloadContent (line 27) | async downloadContent(token, syncLocation) {
  method uploadContent (line 35) | async uploadContent(token, content, syncLocation) {
  method publish (line 54) | async publish(token, html, metadata, publishLocation) {
  method openFile (line 71) | async openFile(token, syncLocation) {
  method makeLocation (line 109) | makeLocation(token, owner, repo, branch, path) {
  method listFileRevisions (line 119) | async listFileRevisions({ token, syncLocation }) {
  method loadFileRevision (line 148) | async loadFileRevision() {
  method getFileRevisionContent (line 152) | async getFileRevisionContent({

FILE: src/services/providers/githubWorkspaceProvider.js
  method getToken (line 15) | getToken() {
  method getWorkspaceParams (line 18) | getWorkspaceParams({
  method getWorkspaceLocationUrl (line 32) | getWorkspaceLocationUrl({
  method getSyncDataUrl (line 40) | getSyncDataUrl({ id }) {
  method getSyncDataDescription (line 44) | getSyncDataDescription({ id }) {
  method initWorkspace (line 47) | async initWorkspace() {
  method getChanges (line 93) | getChanges() {
  method prepareChanges (line 99) | prepareChanges(tree) {
  method saveWorkspaceItem (line 102) | async saveWorkspaceItem({ item }) {
  method removeWorkspaceItem (line 127) | async removeWorkspaceItem({ syncData }) {
  method downloadWorkspaceContent (line 138) | async downloadWorkspaceContent({
  method downloadWorkspaceData (line 160) | async downloadWorkspaceData({ token, syncData }) {
  method uploadWorkspaceContent (line 181) | async uploadWorkspaceContent({ token, content, file }) {
  method uploadWorkspaceData (line 207) | async uploadWorkspaceData({ token, item }) {
  method listFileRevisions (line 229) | async listFileRevisions({ token, fileSyncDataId }) {
  method loadFileRevision (line 263) | async loadFileRevision() {
  method getFileRevisionContent (line 267) | async getFileRevisionContent({

FILE: src/services/providers/gitlabProvider.js
  method getToken (line 13) | getToken({ sub }) {
  method getLocationUrl (line 16) | getLocationUrl({
  method getLocationDescription (line 25) | getLocationDescription({ path }) {
  method downloadContent (line 28) | async downloadContent(token, syncLocation) {
  method uploadContent (line 36) | async uploadContent(token, content, syncLocation) {
  method publish (line 59) | async publish(token, html, metadata, publishLocation) {
  method openFile (line 80) | async openFile(token, syncLocation) {
  method makeLocation (line 123) | makeLocation(token, projectPath, branch, path) {
  method listFileRevisions (line 132) | async listFileRevisions({ token, syncLocation }) {
  method loadFileRevision (line 154) | async loadFileRevision() {
  method getFileRevisionContent (line 158) | async getFileRevisionContent({

FILE: src/services/providers/gitlabWorkspaceProvider.js
  method getToken (line 15) | getToken() {
  method getWorkspaceParams (line 18) | getWorkspaceParams({
  method getWorkspaceLocationUrl (line 32) | getWorkspaceLocationUrl({
  method getSyncDataUrl (line 40) | getSyncDataUrl({ id }) {
  method getSyncDataDescription (line 45) | getSyncDataDescription({ id }) {
  method initWorkspace (line 48) | async initWorkspace() {
  method getChanges (line 105) | getChanges() {
  method prepareChanges (line 111) | prepareChanges(tree) {
  method saveWorkspaceItem (line 117) | async saveWorkspaceItem({ item }) {
  method removeWorkspaceItem (line 142) | async removeWorkspaceItem({ syncData }) {
  method downloadWorkspaceContent (line 153) | async downloadWorkspaceContent({
  method downloadWorkspaceData (line 175) | async downloadWorkspaceData({ token, syncData }) {
  method uploadWorkspaceContent (line 196) | async uploadWorkspaceContent({ token, content, file }) {
  method uploadWorkspaceData (line 223) | async uploadWorkspaceData({ token, item }) {
  method listFileRevisions (line 245) | async listFileRevisions({ token, fileSyncDataId }) {
  method loadFileRevision (line 270) | async loadFileRevision() {
  method getFileRevisionContent (line 274) | async getFileRevisionContent({

FILE: src/services/providers/googleDriveAppDataProvider.js
  method getToken (line 11) | getToken() {
  method getWorkspaceParams (line 14) | getWorkspaceParams() {
  method getWorkspaceLocationUrl (line 18) | getWorkspaceLocationUrl() {
  method getSyncDataUrl (line 22) | getSyncDataUrl() {
  method getSyncDataDescription (line 26) | getSyncDataDescription({ id }) {
  method initWorkspace (line 29) | async initWorkspace() {
  method getChanges (line 34) | async getChanges() {
  method onChangesApplied (line 60) | onChangesApplied() {
  method saveWorkspaceItem (line 65) | async saveWorkspaceItem({ item, syncData, ifNotTooLate }) {
  method removeWorkspaceItem (line 84) | removeWorkspaceItem({ syncData, ifNotTooLate }) {
  method downloadWorkspaceContent (line 88) | async downloadWorkspaceContent({ token, contentSyncData }) {
  method downloadWorkspaceData (line 99) | async downloadWorkspaceData({ token, syncData }) {
  method uploadWorkspaceContent (line 114) | async uploadWorkspaceContent({
  method uploadWorkspaceData (line 142) | async uploadWorkspaceData({
  method listFileRevisions (line 170) | async listFileRevisions({ token, contentSyncDataId }) {
  method loadFileRevision (line 178) | async loadFileRevision() {
  method getFileRevisionContent (line 182) | async getFileRevisionContent({ token, contentSyncDataId, revisionId }) {

FILE: src/services/providers/googleDriveProvider.js
  method getToken (line 10) | getToken({ sub }) {
  method getLocationUrl (line 14) | getLocationUrl({ driveFileId }) {
  method getLocationDescription (line 17) | getLocationDescription({ driveFileId }) {
  method initAction (line 20) | async initAction() {
  method performAction (line 84) | async performAction() {
  method downloadContent (line 104) | async downloadContent(token, syncLocation) {
  method uploadContent (line 108) | async uploadContent(token, content, syncLocation, ifNotTooLate) {
  method publish (line 128) | async publish(token, html, metadata, publishLocation) {
  method openFiles (line 142) | async openFiles(token, driveFiles) {
  method makeLocation (line 181) | makeLocation(token, fileId, folderId) {
  method listFileRevisions (line 194) | async listFileRevisions({ token, syncLocation }) {
  method loadFileRevision (line 202) | async loadFileRevision() {
  method getFileRevisionContent (line 206) | async getFileRevisionContent({

FILE: src/services/providers/googleDriveWorkspaceProvider.js
  method getToken (line 14) | getToken() {
  method getWorkspaceParams (line 17) | getWorkspaceParams({ folderId }) {
  method getWorkspaceLocationUrl (line 23) | getWorkspaceLocationUrl({ folderId }) {
  method getSyncDataUrl (line 26) | getSyncDataUrl({ id }) {
  method getSyncDataDescription (line 29) | getSyncDataDescription({ id }) {
  method initWorkspace (line 32) | async initWorkspace() {
  method performAction (line 144) | async performAction() {
  method getChanges (line 189) | async getChanges() {
  method prepareChanges (line 199) | prepareChanges(changes) {
  method onChangesApplied (line 319) | onChangesApplied() {
  method saveWorkspaceItem (line 324) | async saveWorkspaceItem({ item, syncData, ifNotTooLate }) {
  method removeWorkspaceItem (line 379) | async removeWorkspaceItem({ syncData, ifNotTooLate }) {
  method downloadWorkspaceContent (line 386) | async downloadWorkspaceContent({ token, contentSyncData, fileSyncData }) {
  method downloadWorkspaceData (line 407) | async downloadWorkspaceData({ token, syncData }) {
  method uploadWorkspaceContent (line 422) | async uploadWorkspaceContent({
  method uploadWorkspaceData (line 477) | async uploadWorkspaceData({
  method listFileRevisions (line 513) | async listFileRevisions({ token, fileSyncDataId }) {
  method loadFileRevision (line 521) | async loadFileRevision() {
  method getFileRevisionContent (line 525) | async getFileRevisionContent({

FILE: src/services/providers/helpers/couchdbHelper.js
  method getDb (line 71) | getDb(token) {
  method getChanges (line 78) | async getChanges(token, lastSeq) {
  method uploadDocument (line 109) | async uploadDocument({
  method removeDocument (line 144) | async removeDocument(token, documentId, rev) {
  method retrieveDocument (line 160) | async retrieveDocument(token, documentId, rev) {
  method retrieveDocumentWithAttachments (line 170) | async retrieveDocumentWithAttachments(token, documentId, rev) {
  method retrieveDocumentWithRevisions (line 186) | async retrieveDocumentWithRevisions(token, documentId) {

FILE: src/services/providers/helpers/dropboxHelper.js
  method startOauth2 (line 62) | async startOauth2(fullAccess, sub = null, silent = false) {
  method addAccount (line 101) | async addAccount(fullAccess = false) {
  method uploadFile (line 110) | async uploadFile({
  method downloadFile (line 129) | async downloadFile({
  method listRevisions (line 150) | async listRevisions({
  method openChooser (line 173) | async openChooser(token) {

FILE: src/services/providers/helpers/githubHelper.js
  method startOauth2 (line 65) | async startOauth2(scopes, sub = null, silent = false) {
  method addAccount (line 120) | async addAccount(repoFullAccess = false) {
  method getTree (line 130) | async getTree({
  method getCommits (line 151) | async getCommits({
  method uploadFile (line 168) | async uploadFile({
  method removeFile (line 192) | async removeFile({
  method downloadFile (line 214) | async downloadFile({
  method uploadGist (line 235) | async uploadGist({
  method downloadGist (line 273) | async downloadGist({
  method getGistCommits (line 290) | async getGistCommits({
  method downloadGistRevision (line 303) | async downloadGistRevision({

FILE: src/services/providers/helpers/gitlabHelper.js
  method startOauth2 (line 53) | async startOauth2(serverUrl, applicationId, sub = null, silent = false) {
  method addAccount (line 93) | async addAccount(serverUrl, applicationId, sub = null) {
  method getProjectId (line 102) | async getProjectId(token, { projectPath, projectId }) {
  method getTree (line 116) | async getTree({
  method getCommits (line 134) | async getCommits({
  method uploadFile (line 153) | async uploadFile({
  method removeFile (line 176) | async removeFile({
  method downloadFile (line 197) | async downloadFile({

FILE: src/services/providers/helpers/googleHelper.js
  method $request (line 85) | async $request(token, options) {
  method startOauth2 (line 113) | async startOauth2(scopes, sub = null, silent = false) {
  method refreshToken (line 218) | async refreshToken(token, scopes = []) {
  method signin (line 253) | signin() {
  method addDriveAccount (line 256) | async addDriveAccount(fullAccess = false, sub = null) {
  method addBloggerAccount (line 261) | async addBloggerAccount() {
  method addPhotosAccount (line 266) | async addPhotosAccount() {
  method $uploadFile (line 277) | async $uploadFile({
  method uploadFile (line 350) | async uploadFile({
  method uploadAppDataFile (line 374) | async uploadAppDataFile({
  method getFile (line 395) | async getFile(token, id) {
  method $downloadFile (line 410) | async $downloadFile(refreshedToken, id) {
  method downloadFile (line 417) | async downloadFile(token, id) {
  method downloadAppDataFile (line 421) | async downloadAppDataFile(token, id) {
  method $removeFile (line 429) | async $removeFile(refreshedToken, id, ifNotTooLate = cb => cb()) {
  method removeFile (line 439) | async removeFile(token, id, ifNotTooLate) {
  method removeAppDataFile (line 443) | async removeAppDataFile(token, id, ifNotTooLate = cb => cb()) {
  method $getFileRevisions (line 451) | async $getFileRevisions(refreshedToken, id) {
  method getFileRevisions (line 478) | async getFileRevisions(token, id) {
  method getAppDataFileRevisions (line 482) | async getAppDataFileRevisions(token, id) {
  method $downloadFileRevision (line 490) | async $downloadFileRevision(refreshedToken, id, revisionId) {
  method downloadFileRevision (line 497) | async downloadFileRevision(token, fileId, revisionId) {
  method downloadAppDataFileRevision (line 501) | async downloadAppDataFileRevision(token, fileId, revisionId) {
  method getChanges (line 509) | async getChanges(token, startPageToken, isAppData, teamDriveId = null) {
  method uploadBlogger (line 551) | async uploadBlogger({
  method openPicker (line 625) | async openPicker(token, type = 'doc') {

FILE: src/services/providers/helpers/wordpressHelper.js
  method startOauth2 (line 20) | async startOauth2(sub = null, silent = false) {
  method refreshToken (line 54) | async refreshToken(token) {
  method addAccount (line 69) | async addAccount(fullAccess = false) {
  method uploadPost (line 79) | async uploadPost({

FILE: src/services/providers/helpers/zendeskHelper.js
  method startOauth2 (line 19) | async startOauth2(subdomain, clientId, sub = null, silent = false) {
  method addAccount (line 54) | async addAccount(subdomain, clientId) {
  method uploadArticle (line 63) | async uploadArticle({

FILE: src/services/providers/wordpressProvider.js
  method getToken (line 8) | getToken({ sub }) {
  method getLocationUrl (line 11) | getLocationUrl({ siteId, postId }) {
  method getLocationDescription (line 14) | getLocationDescription({ postId }) {
  method publish (line 17) | async publish(token, html, metadata, publishLocation) {
  method makeLocation (line 30) | makeLocation(token, domain, postId) {

FILE: src/services/providers/zendeskProvider.js
  method getToken (line 8) | getToken({ sub }) {
  method getLocationUrl (line 11) | getLocationUrl({ sub, locale, articleId }) {
  method getLocationDescription (line 15) | getLocationDescription({ articleId }) {
  method publish (line 18) | async publish(token, html, metadata, publishLocation) {
  method makeLocation (line 32) | makeLocation(token, sectionId, locale, articleId) {

FILE: src/services/syncSvc.js
  constant LAST_SEEN (line 24) | const LAST_SEEN = 0;
  constant LAST_MERGED (line 25) | const LAST_MERGED = 1;
  constant LAST_SENT (line 26) | const LAST_SENT = 2;
  class SyncContext (line 294) | class SyncContext {
  method init (line 876) | async init() {

FILE: src/services/tempFileSvc.js
  method setReady (line 16) | setReady() {
  method close (line 23) | close() {
  method init (line 31) | async init() {

FILE: src/services/templateWorker.js
  function arrayToHtml (line 8) | function arrayToHtml(arr) {
  method get (line 71) | get() {
  function safeEval (line 85) | function safeEval(code) {

FILE: src/services/timeSvc.js
  function strftime (line 7) | function strftime(time, formatString) {
  function isDayFirst (line 76) | function isDayFirst() {
  function isYearSeparator (line 97) | function isYearSeparator() {
  function isThisYear (line 119) | function isThisYear(date) {
  class RelativeTime (line 124) | class RelativeTime {
    method constructor (line 125) | constructor(date) {
    method toString (line 129) | toString() {
    method timeElapsed (line 134) | timeElapsed() {
    method formatDate (line 160) | formatDate() {
  method format (line 170) | format(time) {

FILE: src/services/userSvc.js
  method setInfoResolver (line 47) | setInfoResolver(type, subPrefix, resolver) {
  method getCurrentUserId (line 52) | getCurrentUserId() {
  method addUserInfo (line 62) | addUserInfo(userInfo) {
  method addUserId (line 66) | addUserId(userId) {

FILE: src/services/utils.js
  method setQueryParams (line 81) | setQueryParams(params = {}) {
  method sanitizeText (line 90) | sanitizeText(text) {
  method sanitizeName (line 95) | sanitizeName(name) {
  method sanitizeFilename (line 100) | sanitizeFilename(name) {
  method serializeObject (line 107) | serializeObject(obj) {
  method search (line 119) | search(items, criteria) {
  method uid (line 130) | uid() {
  method hash (line 134) | hash(str) {
  method getItemHash (line 145) | getItemHash(item) {
  method addItemHash (line 154) | addItemHash(item) {
  method makeWorkspaceId (line 160) | makeWorkspaceId(params) {
  method getDbName (line 163) | getDbName(workspaceId) {
  method encodeBase64 (line 170) | encodeBase64(str, urlSafe = false) {
  method decodeBase64 (line 185) | decodeBase64(str) {
  method computeProperties (line 195) | computeProperties(yamlProperties) {
  method randomize (line 209) | randomize(value) {
  method setInterval (line 212) | setInterval(func, interval) {
  method awaitSequence (line 215) | async awaitSequence(values, asyncFunc) {
  method awaitSome (line 227) | async awaitSome(asyncFunc) {
  method someResult (line 233) | someResult(values, func) {
  method addQueryParams (line 242) | addQueryParams(url = '', params = {}, hash = false) {
  method resolveUrl (line 267) | resolveUrl(baseUrl, path) {
  method getHostname (line 281) | getHostname(url) {
  method encodeUrlPath (line 285) | encodeUrlPath(path) {
  method parseGithubRepoUrl (line 288) | parseGithubRepoUrl(url) {
  method parseGitlabProjectPath (line 295) | parseGitlabProjectPath(url) {
  method createHiddenIframe (line 299) | createHiddenIframe(url) {
  method wrapRange (line 308) | wrapRange(range, eltProperties) {
  method unwrapRange (line 342) | unwrapRange(eltCollection) {

FILE: src/services/workspaceSvc.js
  method createFile (line 13) | async createFile({
  method storeItem (line 74) | async storeItem(item) {
  method setOrPatchItem (line 117) | setOrPatchItem(patch) {
  method deleteFile (line 152) | deleteFile(fileId) {
  method sanitizeWorkspace (line 172) | sanitizeWorkspace(idsToKeep) {
  method removeCircularReference (line 183) | removeCircularReference(item) {
  method ensureUniquePaths (line 203) | ensureUniquePaths(idsToKeep = {}) {
  method makePathUnique (line 218) | makePathUnique(id) {
  method addSyncLocation (line 248) | addSyncLocation(location) {
  method addPublishLocation (line 262) | addPublishLocation(location) {
  method ensureUniqueLocations (line 279) | ensureUniqueLocations(idsToKeep = {}) {
  method removeWorkspace (line 294) | async removeWorkspace(id) {

FILE: src/store/content.js
  method patchCurrent (line 56) | patchCurrent({ state, getters, commit }, value) {
  method setRevisionContent (line 65) | setRevisionContent({ state, rootGetters, commit }, value) {
  method restoreRevision (line 78) | async restoreRevision({

FILE: src/store/contentState.js
  method patchCurrent (line 14) | patchCurrent({ getters, commit }, value) {

FILE: src/store/contextMenu.js
  method open (line 21) | open({ commit, rootState }, { coordinates, items }) {
  method close (line 50) | close({ commit }) {

FILE: src/store/data.js
  method deleteItem (line 138) | deleteItem({ itemsById }, id) {

FILE: src/store/discussion.js
  method cancelNewComment (line 129) | cancelNewComment({ commit, getters }) {
  method createNewDiscussion (line 135) | async createNewDiscussion({ commit, dispatch, rootGetters }, selection) {
  method cleanCurrentFile (line 153) | cleanCurrentFile({

FILE: src/store/explorer.js
  function debounceAction (line 9) | function debounceAction(action, wait) {
  class Node (line 20) | class Node {
    method constructor (line 21) | constructor(item, locations = [], isFolder = false) {
    method sortChildren (line 31) | sortChildren() {
  function getParent (line 46) | function getParent({ item, isNil }, { nodeMap, rootNode }) {
  function getFolder (line 53) | function getFolder(node, getters) {
  method setNewItem (line 74) | setNewItem(state, item) {
  method setNewItemName (line 77) | setNewItemName(state, name) {
  method toggleOpenNode (line 80) | toggleOpenNode(state, id) {
  method openNode (line 177) | openNode({
  method setDragTarget (line 194) | setDragTarget({ commit, getters, dispatch }, node) {

FILE: src/store/file.js
  method setCurrentId (line 21) | setCurrentId(state, value) {
  method patchCurrent (line 28) | patchCurrent({ getters, commit }, value) {

FILE: src/store/findReplace.js
  method open (line 24) | open({ commit }, { type, findText }) {

FILE: src/store/layout.js
  function computeStyles (line 35) | function computeStyles(state, getters, layoutSettings = getters['data/la...
  method updateBodySize (line 180) | updateBodySize({ commit, dispatch, rootGetters }) {

FILE: src/store/modal.js
  method open (line 19) | async open({ commit, state }, param) {
  method hideUntil (line 31) | async hideUntil({ commit }, promise) {

FILE: src/store/moduleTemplate.js
  method setItem (line 17) | setItem(state, value) {
  method patchItem (line 24) | patchItem(state, patch) {
  method deleteItem (line 34) | deleteItem(state, id) {

FILE: src/store/notification.js
  method showItem (line 17) | showItem({ state, commit }, item) {
  method info (line 48) | info({ dispatch }, content) {
  method badge (line 54) | badge({ dispatch }, content) {
  method confirm (line 60) | confirm({ dispatch }, content) {
  method error (line 67) | error({ dispatch, rootState }, error) {

FILE: src/store/queue.js
  method enqueue (line 22) | enqueue({ state, commit, dispatch }, cb) {
  method enqueueSyncRequest (line 54) | enqueueSyncRequest({ state, commit, dispatch }, cb) {
  method enqueuePublishRequest (line 64) | enqueuePublishRequest({ state, commit, dispatch }, cb) {
  method doWithLocation (line 74) | async doWithLocation({ commit }, { location, action }) {

FILE: test/unit/mocks/cryptoMock.js
  method getRandomValues (line 2) | getRandomValues(array) {

FILE: test/unit/mocks/localStorageMock.js
  method getItem (line 3) | getItem(key) {
  method setItem (line 6) | setItem(key, value) {

FILE: test/unit/mocks/mutationObserverMock.js
  class MutationObserver (line 2) | class MutationObserver {
    method observe (line 3) | observe() {

FILE: test/unit/specs/specUtils.js
  method checkToggler (line 28) | async checkToggler(Component, toggler, checker, featureId) {
  method resolveModal (line 36) | async resolveModal(type) {
  method getContextMenuItem (line 43) | getContextMenuItem(name) {
  method resolveContextMenu (line 46) | async resolveContextMenu(name) {
  method expectBadge (line 52) | async expectBadge(featureId, isEarned = true) {
Condensed preview — 330 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,091K chars).
[
  {
    "path": ".babelrc",
    "chars": 280,
    "preview": "{\n  \"presets\": [\n    [\"env\", { \"modules\": false }],\n    \"stage-2\"\n  ],\n  \"plugins\": [\"transform-runtime\"],\n  \"comments\":"
  },
  {
    "path": ".dockerignore",
    "chars": 32,
    "preview": "node_modules\n.git\ndist\n.history\n"
  },
  {
    "path": ".editorconfig",
    "chars": 147,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": ".eslintignore",
    "chars": 37,
    "preview": "build/*.js\nconfig/*.js\nsrc/libs/*.js\n"
  },
  {
    "path": ".eslintrc.js",
    "chars": 1010,
    "preview": "// http://eslint.org/docs/user-guide/configuring\n\nmodule.exports = {\n  root: true,\n  parser: 'babel-eslint',\n  parserOpt"
  },
  {
    "path": ".gitignore",
    "chars": 119,
    "preview": ".DS_Store\nnode_modules/\ndist/\n.history\n.idea\nnpm-debug.log*\n.vscode\nstackedit_v4\nchrome-app/*.zip\n/test/unit/coverage/\n"
  },
  {
    "path": ".postcssrc.js",
    "chars": 196,
    "preview": "// https://github.com/michael-ciniawsky/postcss-load-config\n\nmodule.exports = {\n  \"plugins\": {\n    // to edit target bro"
  },
  {
    "path": ".stylelintrc",
    "chars": 136,
    "preview": "{\n  \"processors\": [\"stylelint-processor-html\"],\n  \"extends\": \"stylelint-config-standard\",\n  \"rules\": {\n    \"no-empty-sou"
  },
  {
    "path": ".travis.yml",
    "chars": 373,
    "preview": "language: node_js\n\nnode_js:\n  - \"12\"\n\nservices:\n  - docker\n\nbefore_deploy:\n  # Run docker build\n  - docker build -t benw"
  },
  {
    "path": "Dockerfile",
    "chars": 307,
    "preview": "FROM benweet/stackedit-base\n\nRUN mkdir -p /opt/stackedit\nWORKDIR /opt/stackedit\n\nCOPY package*json /opt/stackedit/\nCOPY "
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 3083,
    "preview": "# StackEdit\n\n[![Build Status](https://img.shields.io/travis/benweet/stackedit.svg?style=flat)](https://travis-ci.org/ben"
  },
  {
    "path": "build/build.js",
    "chars": 953,
    "preview": "require('./check-versions')()\n\nprocess.env.NODE_ENV = 'production'\n\nvar ora = require('ora')\nvar rm = require('rimraf')\n"
  },
  {
    "path": "build/check-versions.js",
    "chars": 1257,
    "preview": "var chalk = require('chalk')\nvar semver = require('semver')\nvar packageConfig = require('../package.json')\nvar shell = r"
  },
  {
    "path": "build/deploy.sh",
    "chars": 787,
    "preview": "#!/bin/bash\nset -e\n\n# Tag and push docker image\ndocker login -u benweet -p \"$DOCKER_PASSWORD\"\ndocker tag benweet/stacked"
  },
  {
    "path": "build/dev-client.js",
    "chars": 245,
    "preview": "/* eslint-disable */\nrequire('eventsource-polyfill')\nvar hotClient = require('webpack-hot-middleware/client?noInfo=true&"
  },
  {
    "path": "build/dev-server.js",
    "chars": 2550,
    "preview": "require('./check-versions')()\n\nvar config = require('../config')\nObject.keys(config.dev.env).forEach((key) => {\n  if (!p"
  },
  {
    "path": "build/utils.js",
    "chars": 1949,
    "preview": "var path = require('path')\nvar config = require('../config')\nvar ExtractTextPlugin = require('extract-text-webpack-plugi"
  },
  {
    "path": "build/vue-loader.conf.js",
    "chars": 307,
    "preview": "var utils = require('./utils')\nvar config = require('../config')\nvar isProduction = process.env.NODE_ENV === 'production"
  },
  {
    "path": "build/webpack.base.conf.js",
    "chars": 2792,
    "preview": "var path = require('path')\nvar webpack = require('webpack')\nvar utils = require('./utils')\nvar config = require('../conf"
  },
  {
    "path": "build/webpack.dev.conf.js",
    "chars": 1210,
    "preview": "var utils = require('./utils')\nvar webpack = require('webpack')\nvar config = require('../config')\nvar merge = require('w"
  },
  {
    "path": "build/webpack.prod.conf.js",
    "chars": 4880,
    "preview": "var path = require('path')\nvar utils = require('./utils')\nvar webpack = require('webpack')\nvar config = require('../conf"
  },
  {
    "path": "build/webpack.style.conf.js",
    "chars": 1442,
    "preview": "var path = require('path')\nvar utils = require('./utils')\nvar webpack = require('webpack')\nvar utils = require('./utils'"
  },
  {
    "path": "chart/.helmignore",
    "chars": 342,
    "preview": "# Patterns to ignore when building packages.\n# This supports shell glob matching, relative path matching, and\n# negation"
  },
  {
    "path": "chart/Chart.yaml",
    "chars": 129,
    "preview": "apiVersion: v1\nappVersion: vSTACKEDIT_VERSION\ndescription: In-browser Markdown editor\nname: stackedit\nversion: STACKEDIT"
  },
  {
    "path": "chart/templates/NOTES.txt",
    "chars": 1519,
    "preview": "1. Get the application URL by running these commands:\n{{- if .Values.ingress.enabled }}\n{{- range $host := .Values.ingre"
  },
  {
    "path": "chart/templates/_helpers.tpl",
    "chars": 1427,
    "preview": "{{/* vim: set filetype=mustache: */}}\n{{/*\nExpand the name of the chart.\n*/}}\n{{- define \"stackedit.name\" -}}\n{{- defaul"
  },
  {
    "path": "chart/templates/deployment.yaml",
    "chars": 2807,
    "preview": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n  name: {{ include \"stackedit.fullname\" . }}\n  labels:\n{{ include \"stacke"
  },
  {
    "path": "chart/templates/ingress.yaml",
    "chars": 910,
    "preview": "{{- if .Values.ingress.enabled -}}\n{{- $fullName := include \"stackedit.fullname\" . -}}\napiVersion: networking.k8s.io/v1\n"
  },
  {
    "path": "chart/templates/service.yaml",
    "chars": 414,
    "preview": "apiVersion: v1\nkind: Service\nmetadata:\n  name: {{ include \"stackedit.fullname\" . }}\n  labels:\n{{ include \"stackedit.labe"
  },
  {
    "path": "chart/templates/tests/test-connection.yaml",
    "chars": 388,
    "preview": "apiVersion: v1\nkind: Pod\nmetadata:\n  name: \"{{ include \"stackedit.fullname\" . }}-test-connection\"\n  labels:\n{{ include \""
  },
  {
    "path": "chart/values.yaml",
    "chars": 1427,
    "preview": "# Default values for stackedit.\n# This is a YAML-formatted file.\n# Declare variables to be passed into your templates.\n\n"
  },
  {
    "path": "chrome-app/manifest.json",
    "chars": 578,
    "preview": "{\n  \"name\": \"StackEdit\",\n  \"description\": \"In-browser Markdown editor\",\n  \"version\": \"1.0.13\",\n  \"manifest_version\": 2,\n"
  },
  {
    "path": "config/dev.env.js",
    "chars": 139,
    "preview": "var merge = require('webpack-merge')\nvar prodEnv = require('./prod.env')\n\nmodule.exports = merge(prodEnv, {\n  NODE_ENV: "
  },
  {
    "path": "config/index.js",
    "chars": 1464,
    "preview": "// see http://vuejs-templates.github.io/webpack for documentation.\nvar path = require('path')\n\nmodule.exports = {\n  buil"
  },
  {
    "path": "config/prod.env.js",
    "chars": 48,
    "preview": "module.exports = {\n  NODE_ENV: '\"production\"'\n}\n"
  },
  {
    "path": "gulpfile.js",
    "chars": 695,
    "preview": "const path = require('path');\nconst gulp = require('gulp');\nconst concat = require('gulp-concat');\n\nconst prismScripts ="
  },
  {
    "path": "index.html",
    "chars": 642,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <title>StackEdit</title>\n    <link rel=\"canonical\" href=\""
  },
  {
    "path": "index.js",
    "chars": 597,
    "preview": "const env = require('./config/prod.env');\n\nObject.keys(env).forEach((key) => {\n  if (!process.env[key]) {\n    process.en"
  },
  {
    "path": "package.json",
    "chars": 4764,
    "preview": "{\n  \"name\": \"stackedit\",\n  \"version\": \"5.15.4\",\n  \"description\": \"Free, open-source, full-featured Markdown editor\",\n  \""
  },
  {
    "path": "server/conf.js",
    "chars": 1162,
    "preview": "const pandocPath = process.env.PANDOC_PATH || 'pandoc';\nconst wkhtmltopdfPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmlt"
  },
  {
    "path": "server/github.js",
    "chars": 931,
    "preview": "const qs = require('qs'); // eslint-disable-line import/no-extraneous-dependencies\nconst request = require('request');\nc"
  },
  {
    "path": "server/index.js",
    "chars": 2165,
    "preview": "const compression = require('compression');\nconst serveStatic = require('serve-static');\nconst bodyParser = require('bod"
  },
  {
    "path": "server/pandoc.js",
    "chars": 4253,
    "preview": "/* global window */\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst tmp = require('tmp');\nco"
  },
  {
    "path": "server/pdf.js",
    "chars": 5679,
    "preview": "/* global window,MathJax */\nconst { spawn } = require('child_process');\nconst fs = require('fs');\nconst tmp = require('t"
  },
  {
    "path": "server/user.js",
    "chars": 3163,
    "preview": "const request = require('request');\nconst AWS = require('aws-sdk');\nconst verifier = require('google-id-token-verifier')"
  },
  {
    "path": "src/components/App.vue",
    "chars": 1683,
    "preview": "<template>\n  <div class=\"app\" :class=\"classes\" @keydown.esc=\"close\">\n    <splash-screen v-if=\"!ready\"></splash-screen>\n "
  },
  {
    "path": "src/components/ButtonBar.vue",
    "chars": 3077,
    "preview": "<template>\n  <div class=\"button-bar\">\n    <div class=\"button-bar__inner button-bar__inner--top\">\n      <button class=\"bu"
  },
  {
    "path": "src/components/CodeEditor.vue",
    "chars": 1246,
    "preview": "<template>\n  <pre class=\"code-editor textfield prism\" :disabled=\"disabled\"></pre>\n</template>\n\n<script>\nimport Prism fro"
  },
  {
    "path": "src/components/ContextMenu.vue",
    "chars": 1820,
    "preview": "<template>\n  <div class=\"context-menu\" v-if=\"items.length\" @click=\"close()\" @contextmenu.prevent=\"close()\">\n    <div cla"
  },
  {
    "path": "src/components/Editor.vue",
    "chars": 3239,
    "preview": "<template>\n  <div class=\"editor\">\n    <pre class=\"editor__inner markdown-highlighting\" :style=\"{padding: styles.editorPa"
  },
  {
    "path": "src/components/Explorer.vue",
    "chars": 2706,
    "preview": "<template>\n  <div class=\"explorer flex flex--column\">\n    <div class=\"side-title flex flex--row flex--space-between\">\n  "
  },
  {
    "path": "src/components/ExplorerNode.vue",
    "chars": 8607,
    "preview": "<template>\n  <div class=\"explorer-node\" :class=\"{'explorer-node--selected': isSelected, 'explorer-node--folder': node.is"
  },
  {
    "path": "src/components/FindReplace.vue",
    "chars": 11740,
    "preview": "<template>\n  <div class=\"find-replace\" @keydown.esc.stop=\"onEscape\">\n    <button class=\"find-replace__close-button butto"
  },
  {
    "path": "src/components/Layout.vue",
    "chars": 7455,
    "preview": "<template>\n  <div class=\"layout\" :class=\"{'layout--revision': revisionContent}\">\n    <div class=\"layout__panel flex flex"
  },
  {
    "path": "src/components/Modal.vue",
    "chars": 11888,
    "preview": "<template>\n  <div class=\"modal\" v-if=\"config\" @keydown.esc.stop=\"onEscape\" @keydown.tab=\"onTab\" @focusin=\"onFocusInOut\" "
  },
  {
    "path": "src/components/NavigationBar.vue",
    "chars": 14491,
    "preview": "<template>\n  <nav class=\"navigation-bar\" :class=\"{'navigation-bar--editor': styles.showEditor && !revisionContent, 'navi"
  },
  {
    "path": "src/components/Notification.vue",
    "chars": 1580,
    "preview": "<template>\n  <div class=\"notification\">\n    <div class=\"notification__item flex flex--row flex--align-center\" v-for=\"(it"
  },
  {
    "path": "src/components/Preview.vue",
    "chars": 4514,
    "preview": "<template>\n  <div class=\"preview\">\n    <div class=\"preview__inner-1\" @click=\"onClick\" @scroll=\"onScroll\">\n      <div cla"
  },
  {
    "path": "src/components/SideBar.vue",
    "chars": 4286,
    "preview": "<template>\n  <div class=\"side-bar flex flex--column\">\n    <div class=\"side-title flex flex--row\">\n      <button v-if=\"pa"
  },
  {
    "path": "src/components/SplashScreen.vue",
    "chars": 344,
    "preview": "<template>\n  <div class=\"splash-screen\">\n    <div class=\"splash-screen__inner logo-background\"></div>\n  </div>\n</templat"
  },
  {
    "path": "src/components/StatusBar.vue",
    "chars": 3410,
    "preview": "<template>\n  <div class=\"stat-panel panel no-overflow\">\n    <div class=\"stat-panel__block stat-panel__block--left\" v-if="
  },
  {
    "path": "src/components/Toc.vue",
    "chars": 3479,
    "preview": "<template>\n  <div class=\"toc\">\n    <div class=\"toc__mask\" :style=\"{top: (maskY - 5) + 'px'}\"></div>\n    <div class=\"toc_"
  },
  {
    "path": "src/components/Tour.vue",
    "chars": 6130,
    "preview": "<template>\n  <div class=\"tour\" @keydown.esc.stop=\"skip\">\n    <div class=\"tour-step\" :class=\"'tour-step--' + step\" :style"
  },
  {
    "path": "src/components/UserImage.vue",
    "chars": 832,
    "preview": "<template>\n  <div class=\"user-image\" :style=\"{backgroundImage: url}\">\n  </div>\n</template>\n\n<script>\nimport userSvc from"
  },
  {
    "path": "src/components/UserName.vue",
    "chars": 587,
    "preview": "<template>\n  <span class=\"user-name\">{{name}}</span>\n</template>\n\n<script>\nimport userSvc from '../services/userSvc';\nim"
  },
  {
    "path": "src/components/common/EditorClassApplier.js",
    "chars": 2622,
    "preview": "import cledit from '../../services/editor/cledit';\nimport editorSvc from '../../services/editorSvc';\nimport utils from '"
  },
  {
    "path": "src/components/common/PreviewClassApplier.js",
    "chars": 2616,
    "preview": "import cledit from '../../services/editor/cledit';\nimport editorSvc from '../../services/editorSvc';\nimport utils from '"
  },
  {
    "path": "src/components/common/vueGlobals.js",
    "chars": 1704,
    "preview": "import Vue from 'vue';\nimport Clipboard from 'clipboard';\nimport timeSvc from '../../services/timeSvc';\nimport store fro"
  },
  {
    "path": "src/components/gutters/Comment.vue",
    "chars": 2768,
    "preview": "<template>\n  <div class=\"comment\">\n    <div class=\"comment__header flex flex--row flex--space-between flex--align-center"
  },
  {
    "path": "src/components/gutters/CommentList.vue",
    "chars": 8828,
    "preview": "<template>\n  <div class=\"comment-list\" :class=\"stickyComment && 'comment-list--' + stickyComment\" :style=\"{width: consta"
  },
  {
    "path": "src/components/gutters/CurrentDiscussion.vue",
    "chars": 5357,
    "preview": "<template>\n  <div class=\"current-discussion\" :style=\"{width: constants.gutterWidth + 'px'}\">\n    <sticky-comment v-if=\"s"
  },
  {
    "path": "src/components/gutters/EditorNewDiscussionButton.vue",
    "chars": 1771,
    "preview": "<template>\n  <a class=\"new-discussion-button\" href=\"javascript:void(0)\" v-if=\"coordinates\" :style=\"{top: coordinates.top"
  },
  {
    "path": "src/components/gutters/NewComment.vue",
    "chars": 5749,
    "preview": "<template>\n  <div class=\"comment comment--new\" @keydown.esc.stop=\"cancelNewComment\">\n    <div class=\"comment__header fle"
  },
  {
    "path": "src/components/gutters/PreviewNewDiscussionButton.vue",
    "chars": 1674,
    "preview": "<template>\n  <a class=\"new-discussion-button\" href=\"javascript:void(0)\" v-if=\"coordinates\" :style=\"{top: coordinates.top"
  },
  {
    "path": "src/components/gutters/StickyComment.vue",
    "chars": 963,
    "preview": "<template>\n  <div class=\"sticky-comment\" :style=\"{width: constants.gutterWidth + 'px', top: top + 'px'}\">\n    <comment v"
  },
  {
    "path": "src/components/menus/HistoryMenu.vue",
    "chars": 13296,
    "preview": "<template>\n  <div class=\"history side-bar__panel side-bar__panel--menu\">\n    <div class=\"side-bar__info\">\n      <p v-if="
  },
  {
    "path": "src/components/menus/ImportExportMenu.vue",
    "chars": 4598,
    "preview": "<template>\n  <div class=\"side-bar__panel side-bar__panel--menu\">\n    <input class=\"hidden-file\" id=\"import-markdown-file"
  },
  {
    "path": "src/components/menus/MainMenu.vue",
    "chars": 8638,
    "preview": "<template>\n  <div class=\"side-bar__panel side-bar__panel--menu\">\n    <div class=\"side-bar__info\">\n      <div class=\"menu"
  },
  {
    "path": "src/components/menus/PublishMenu.vue",
    "chars": 10280,
    "preview": "<template>\n  <div class=\"side-bar__panel side-bar__panel--menu\">\n    <div class=\"side-bar__info\" v-if=\"isCurrentTemp\">\n "
  },
  {
    "path": "src/components/menus/SyncMenu.vue",
    "chars": 10109,
    "preview": "<template>\n  <div class=\"side-bar__panel side-bar__panel--menu\">\n    <div class=\"side-bar__info\" v-if=\"isCurrentTemp\">\n "
  },
  {
    "path": "src/components/menus/WorkspaceBackupMenu.vue",
    "chars": 1960,
    "preview": "<template>\n  <div class=\"side-bar__panel side-bar__panel--menu\">\n    <input class=\"hidden-file\" id=\"import-backup-file-i"
  },
  {
    "path": "src/components/menus/WorkspacesMenu.vue",
    "chars": 3487,
    "preview": "<template>\n  <div class=\"side-bar__panel side-bar__panel--menu\">\n    <menu-entry @click.native=\"manageWorkspaces\">\n     "
  },
  {
    "path": "src/components/menus/common/MenuEntry.vue",
    "chars": 1531,
    "preview": "<template>\n  <a class=\"menu-entry button flex flex--row flex--align-center\" href=\"javascript:void(0)\">\n    <div class=\"m"
  },
  {
    "path": "src/components/modals/AboutModal.vue",
    "chars": 2434,
    "preview": "<template>\n  <modal-inner class=\"modal__inner-1--about-modal\" aria-label=\"About\">\n    <div class=\"modal__content\">\n     "
  },
  {
    "path": "src/components/modals/AccountManagementModal.vue",
    "chars": 9016,
    "preview": "<template>\n  <modal-inner class=\"modal__inner-1--account-management\" aria-label=\"Manage external accounts\">\n    <div cla"
  },
  {
    "path": "src/components/modals/BadgeManagementModal.vue",
    "chars": 3217,
    "preview": "<template>\n  <modal-inner class=\"modal__inner-1--badge-management\" aria-label=\"Manage badges\">\n    <div class=\"modal__co"
  },
  {
    "path": "src/components/modals/FilePropertiesModal.vue",
    "chars": 9176,
    "preview": "<template>\n  <modal-inner class=\"modal__inner-1--file-properties\" aria-label=\"File properties\">\n    <div class=\"modal__c"
  },
  {
    "path": "src/components/modals/HtmlExportModal.vue",
    "chars": 2172,
    "preview": "<template>\n  <modal-inner aria-label=\"Export to HTML\">\n    <div class=\"modal__content\">\n      <p>Please choose a templat"
  },
  {
    "path": "src/components/modals/ImageModal.vue",
    "chars": 2541,
    "preview": "<template>\n  <modal-inner aria-label=\"Insert image\">\n    <div class=\"modal__content\">\n      <p>Please provide a <b>URL</"
  },
  {
    "path": "src/components/modals/LinkModal.vue",
    "chars": 1094,
    "preview": "<template>\n  <modal-inner aria-label=\"Insert link\">\n    <div class=\"modal__content\">\n      <p>Please provide a <b>URL</b"
  },
  {
    "path": "src/components/modals/PandocExportModal.vue",
    "chars": 2996,
    "preview": "<template>\n  <modal-inner aria-label=\"Export with Pandoc\">\n    <div class=\"modal__content\">\n      <p>Please choose a for"
  },
  {
    "path": "src/components/modals/PdfExportModal.vue",
    "chars": 2737,
    "preview": "<template>\n  <modal-inner aria-label=\"Export to PDF\">\n    <div class=\"modal__content\">\n      <p>Please choose a template"
  },
  {
    "path": "src/components/modals/PublishManagementModal.vue",
    "chars": 4178,
    "preview": "<template>\n  <modal-inner class=\"modal__inner-1--publish-management\" aria-label=\"Manage publication locations\">\n    <div"
  },
  {
    "path": "src/components/modals/SettingsModal.vue",
    "chars": 3636,
    "preview": "<template>\n  <modal-inner class=\"modal__inner-1--settings\" aria-label=\"Settings\">\n    <div class=\"modal__content\">\n     "
  },
  {
    "path": "src/components/modals/SponsorModal.vue",
    "chars": 2483,
    "preview": "<template>\n  <modal-inner class=\"modal__inner-1--sponsor\" aria-label=\"Sponsor\">\n    <div class=\"modal__content\">\n      <"
  },
  {
    "path": "src/components/modals/SyncManagementModal.vue",
    "chars": 4267,
    "preview": "<template>\n  <modal-inner class=\"modal__inner-1--sync-management\" aria-label=\"Manage synchronized locations\">\n    <div c"
  },
  {
    "path": "src/components/modals/TemplatesModal.vue",
    "chars": 6637,
    "preview": "<template>\n  <modal-inner class=\"modal__inner-1--templates\" aria-label=\"Manage templates\">\n    <div class=\"modal__conten"
  },
  {
    "path": "src/components/modals/WorkspaceManagementModal.vue",
    "chars": 6869,
    "preview": "<template>\n  <modal-inner class=\"modal__inner-1--workspace-management\" aria-label=\"Manage workspaces\">\n    <div class=\"m"
  },
  {
    "path": "src/components/modals/common/FormEntry.vue",
    "chars": 556,
    "preview": "<template>\n  <div class=\"form-entry\" :error=\"error\">\n    <label class=\"form-entry__label\" :for=\"uid\">{{label}}<span clas"
  },
  {
    "path": "src/components/modals/common/ModalInner.vue",
    "chars": 745,
    "preview": "<template>\n  <div class=\"modal__inner-1\" role=\"dialog\">\n    <div class=\"modal__inner-2\">\n      <button class=\"modal__clo"
  },
  {
    "path": "src/components/modals/common/Tab.vue",
    "chars": 310,
    "preview": "<template>\n  <div class=\"tabs__tab flex flex--row\" :class=\"{'tabs__tab--active': active}\" role=\"tab\">\n    <a class=\"flex"
  },
  {
    "path": "src/components/modals/common/modalTemplate.js",
    "chars": 2484,
    "preview": "import ModalInner from './ModalInner';\nimport FormEntry from './FormEntry';\nimport store from '../../../store';\n\nconst c"
  },
  {
    "path": "src/components/modals/providers/BloggerPagePublishModal.vue",
    "chars": 2392,
    "preview": "<template>\n  <modal-inner aria-label=\"Publish to Blogger Page\">\n    <div class=\"modal__content\">\n      <div class=\"modal"
  },
  {
    "path": "src/components/modals/providers/BloggerPublishModal.vue",
    "chars": 2440,
    "preview": "<template>\n  <modal-inner aria-label=\"Publish to Blogger\">\n    <div class=\"modal__content\">\n      <div class=\"modal__ima"
  },
  {
    "path": "src/components/modals/providers/CouchdbCredentialsModal.vue",
    "chars": 1603,
    "preview": "<template>\n  <modal-inner aria-label=\"Insert image\">\n    <div class=\"modal__content\">\n      <div class=\"modal__image\">\n "
  },
  {
    "path": "src/components/modals/providers/CouchdbWorkspaceModal.vue",
    "chars": 1576,
    "preview": "<template>\n  <modal-inner aria-label=\"Add CouchDB workspace\">\n    <div class=\"modal__content\">\n      <div class=\"modal__"
  },
  {
    "path": "src/components/modals/providers/DropboxAccountModal.vue",
    "chars": 1084,
    "preview": "<template>\n  <modal-inner aria-label=\"Link Dropbox account\">\n    <div class=\"modal__content\">\n      <div class=\"modal__i"
  },
  {
    "path": "src/components/modals/providers/DropboxPublishModal.vue",
    "chars": 2101,
    "preview": "<template>\n  <modal-inner aria-label=\"Publish to Dropbox\">\n    <div class=\"modal__content\">\n      <div class=\"modal__ima"
  },
  {
    "path": "src/components/modals/providers/DropboxSaveModal.vue",
    "chars": 1526,
    "preview": "<template>\n  <modal-inner aria-label=\"Synchronize with Dropbox\">\n    <div class=\"modal__content\">\n      <div class=\"moda"
  },
  {
    "path": "src/components/modals/providers/GistPublishModal.vue",
    "chars": 2671,
    "preview": "<template>\n  <modal-inner aria-label=\"Publish to Gist\">\n    <div class=\"modal__content\">\n      <div class=\"modal__image\""
  },
  {
    "path": "src/components/modals/providers/GistSyncModal.vue",
    "chars": 1935,
    "preview": "<template>\n  <modal-inner aria-label=\"Synchronize with Gist\">\n    <div class=\"modal__content\">\n      <div class=\"modal__"
  },
  {
    "path": "src/components/modals/providers/GithubAccountModal.vue",
    "chars": 933,
    "preview": "<template>\n  <modal-inner aria-label=\"Link GitHub account\">\n    <div class=\"modal__content\">\n      <div class=\"modal__im"
  },
  {
    "path": "src/components/modals/providers/GithubOpenModal.vue",
    "chars": 2293,
    "preview": "<template>\n  <modal-inner aria-label=\"Synchronize with GitHub\">\n    <div class=\"modal__content\">\n      <div class=\"modal"
  },
  {
    "path": "src/components/modals/providers/GithubPublishModal.vue",
    "chars": 2972,
    "preview": "<template>\n  <modal-inner aria-label=\"Publish to GitHub\">\n    <div class=\"modal__content\">\n      <div class=\"modal__imag"
  },
  {
    "path": "src/components/modals/providers/GithubSaveModal.vue",
    "chars": 2403,
    "preview": "<template>\n  <modal-inner aria-label=\"Synchronize with GitHub\">\n    <div class=\"modal__content\">\n      <div class=\"modal"
  },
  {
    "path": "src/components/modals/providers/GithubWorkspaceModal.vue",
    "chars": 2211,
    "preview": "<template>\n  <modal-inner aria-label=\"Synchronize with GitHub\">\n    <div class=\"modal__content\">\n      <div class=\"modal"
  },
  {
    "path": "src/components/modals/providers/GitlabAccountModal.vue",
    "chars": 2384,
    "preview": "<template>\n  <modal-inner aria-label=\"GitLab account\">\n    <div class=\"modal__content\">\n      <div class=\"modal__image\">"
  },
  {
    "path": "src/components/modals/providers/GitlabOpenModal.vue",
    "chars": 2290,
    "preview": "<template>\n  <modal-inner aria-label=\"Synchronize with GitLab\">\n    <div class=\"modal__content\">\n      <div class=\"modal"
  },
  {
    "path": "src/components/modals/providers/GitlabPublishModal.vue",
    "chars": 2969,
    "preview": "<template>\n  <modal-inner aria-label=\"Publish to GitLab\">\n    <div class=\"modal__content\">\n      <div class=\"modal__imag"
  },
  {
    "path": "src/components/modals/providers/GitlabSaveModal.vue",
    "chars": 2400,
    "preview": "<template>\n  <modal-inner aria-label=\"Synchronize with GitLab\">\n    <div class=\"modal__content\">\n      <div class=\"modal"
  },
  {
    "path": "src/components/modals/providers/GitlabWorkspaceModal.vue",
    "chars": 2325,
    "preview": "<template>\n  <modal-inner aria-label=\"Synchronize with GitLab\">\n    <div class=\"modal__content\">\n      <div class=\"modal"
  },
  {
    "path": "src/components/modals/providers/GoogleDriveAccountModal.vue",
    "chars": 1107,
    "preview": "<template>\n  <modal-inner aria-label=\"Link Google Drive account\">\n    <div class=\"modal__content\">\n      <div class=\"mod"
  },
  {
    "path": "src/components/modals/providers/GoogleDrivePublishModal.vue",
    "chars": 3511,
    "preview": "<template>\n  <modal-inner aria-label=\"Publish to Google Drive\">\n    <div class=\"modal__content\">\n      <div class=\"modal"
  },
  {
    "path": "src/components/modals/providers/GoogleDriveSaveModal.vue",
    "chars": 2321,
    "preview": "<template>\n  <modal-inner aria-label=\"Synchronize with Google Drive\">\n    <div class=\"modal__content\">\n      <div class="
  },
  {
    "path": "src/components/modals/providers/GoogleDriveWorkspaceModal.vue",
    "chars": 1977,
    "preview": "<template>\n  <modal-inner aria-label=\"Add Google Drive workspace\">\n    <div class=\"modal__content\">\n      <div class=\"mo"
  },
  {
    "path": "src/components/modals/providers/GooglePhotoModal.vue",
    "chars": 1925,
    "preview": "<template>\n  <modal-inner class=\"modal__inner-1--google-photo\" aria-label=\"Import Google Photo\">\n    <div class=\"modal__"
  },
  {
    "path": "src/components/modals/providers/WordpressPublishModal.vue",
    "chars": 2620,
    "preview": "<template>\n  <modal-inner aria-label=\"Publish to WordPress\">\n    <div class=\"modal__content\">\n      <div class=\"modal__i"
  },
  {
    "path": "src/components/modals/providers/ZendeskAccountModal.vue",
    "chars": 2143,
    "preview": "<template>\n  <modal-inner aria-label=\"Link Zendesk account\">\n    <div class=\"modal__content\">\n      <div class=\"modal__i"
  },
  {
    "path": "src/components/modals/providers/ZendeskPublishModal.vue",
    "chars": 2847,
    "preview": "<template>\n  <modal-inner aria-label=\"Publish to Zendesk\">\n    <div class=\"modal__content\">\n      <div class=\"modal__ima"
  },
  {
    "path": "src/data/constants.js",
    "chars": 551,
    "preview": "const origin = `${window.location.protocol}//${window.location.host}`;\n\nexport default {\n  cleanTrashAfter: 7 * 24 * 60 "
  },
  {
    "path": "src/data/defaults/defaultLayoutSettings.js",
    "chars": 314,
    "preview": "export default () => ({\n  showNavigationBar: true,\n  showEditor: true,\n  showSidePreview: true,\n  showStatusBar: true,\n "
  },
  {
    "path": "src/data/defaults/defaultLocalSettings.js",
    "chars": 1030,
    "preview": "export default () => ({\n  welcomeFileHashes: {},\n  filePropertiesTab: '',\n  htmlExportTemplate: 'styledHtml',\n  pdfExpor"
  },
  {
    "path": "src/data/defaults/defaultSettings.yml",
    "chars": 2031,
    "preview": "# light or dark\ncolorTheme: light\n# Adjust font size in editor and preview\nfontSizeFactor: 1\n# Adjust maximum text width"
  },
  {
    "path": "src/data/defaults/defaultWorkspaces.js",
    "chars": 157,
    "preview": "export default () => ({\n  main: {\n    id: 'main',\n    name: 'Main workspace',\n    // The rest will be filled by the work"
  },
  {
    "path": "src/data/empties/emptyContent.js",
    "chars": 142,
    "preview": "export default (id = null) => ({\n  id,\n  type: 'content',\n  text: '\\n',\n  properties: '\\n',\n  discussions: {},\n  comment"
  },
  {
    "path": "src/data/empties/emptyContentState.js",
    "chars": 142,
    "preview": "export default (id = null) => ({\n  id,\n  type: 'contentState',\n  selectionStart: 0,\n  selectionEnd: 0,\n  scrollPosition:"
  },
  {
    "path": "src/data/empties/emptyFile.js",
    "chars": 100,
    "preview": "export default (id = null) => ({\n  id,\n  type: 'file',\n  name: '',\n  parentId: null,\n  hash: 0,\n});\n"
  },
  {
    "path": "src/data/empties/emptyFolder.js",
    "chars": 102,
    "preview": "export default (id = null) => ({\n  id,\n  type: 'folder',\n  name: '',\n  parentId: null,\n  hash: 0,\n});\n"
  },
  {
    "path": "src/data/empties/emptyPublishLocation.js",
    "chars": 137,
    "preview": "export default (id = null) => ({\n  id,\n  type: 'publishLocation',\n  providerId: null,\n  fileId: null,\n  templateId: null"
  },
  {
    "path": "src/data/empties/emptySyncLocation.js",
    "chars": 114,
    "preview": "export default (id = null) => ({\n  id,\n  type: 'syncLocation',\n  providerId: null,\n  fileId: null,\n  hash: 0,\n});\n"
  },
  {
    "path": "src/data/empties/emptySyncedContent.js",
    "chars": 125,
    "preview": "export default (id = null) => ({\n  id,\n  type: 'syncedContent',\n  historyData: {},\n  syncHistory: {},\n  v: 0,\n  hash: 0,"
  },
  {
    "path": "src/data/empties/emptyTemplateHelpers.js",
    "chars": 359,
    "preview": "/* Add your custom Handlebars helpers here.\n\nFor example:\n\nHandlebars.registerHelper('transform', function (options) {\n "
  },
  {
    "path": "src/data/empties/emptyTemplateValue.html",
    "chars": 815,
    "preview": "<!-- Specify your Handlebars template here.\n\nThe following JavaScript context will be passed to the template:\n\n{\n  files"
  },
  {
    "path": "src/data/faq.md",
    "chars": 575,
    "preview": "**Where is my data stored?**\n\nIf your workspace is not synced, your files are stored inside your browser and nowhere els"
  },
  {
    "path": "src/data/features.js",
    "chars": 15239,
    "preview": "class Feature {\n  constructor(id, badgeName, description, children = null) {\n    this.id = id;\n    this.badgeName = badg"
  },
  {
    "path": "src/data/markdownSample.md",
    "chars": 1413,
    "preview": "Headers\n---------------------------\n\n# Header 1\n\n## Header 2\n\n### Header 3\n\n\n\nStyling\n---------------------------\n\n*Emph"
  },
  {
    "path": "src/data/pagedownButtons.js",
    "chars": 888,
    "preview": "export default [{\n}, {\n  method: 'bold',\n  title: 'Bold',\n  icon: 'format-bold',\n}, {\n  method: 'italic',\n  title: 'Ital"
  },
  {
    "path": "src/data/presets.js",
    "chars": 1802,
    "preview": "const zero = {\n  // Markdown extensions\n  markdown: {\n    abbr: false,\n    breaks: false,\n    deflist: false,\n    del: f"
  },
  {
    "path": "src/data/simpleModals.js",
    "chars": 3086,
    "preview": "const simpleModal = (contentHtml, rejectText, resolveText) => ({\n  contentHtml: typeof contentHtml === 'function' ? cont"
  },
  {
    "path": "src/data/templates/jekyllSiteTemplate.html",
    "chars": 73,
    "preview": "---\n{{{files.0.content.yamlProperties}}}\n---\n\n{{{files.0.content.html}}}\n"
  },
  {
    "path": "src/data/templates/plainHtmlTemplate.html",
    "chars": 27,
    "preview": "{{{files.0.content.html}}}\n"
  },
  {
    "path": "src/data/templates/styledHtmlTemplate.html",
    "chars": 413,
    "preview": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-sca"
  },
  {
    "path": "src/data/templates/styledHtmlWithTocTemplate.html",
    "chars": 611,
    "preview": "<!DOCTYPE html>\n<html>\n\n<head>\n  <meta charset=\"utf-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-sca"
  },
  {
    "path": "src/data/welcomeFile.md",
    "chars": 7173,
    "preview": "# Welcome to StackEdit!\n\nHi! I'm your first Markdown file in **StackEdit**. If you want to learn about StackEdit, you ca"
  },
  {
    "path": "src/extensions/abcExtension.js",
    "chars": 684,
    "preview": "import renderAbc from 'abcjs/src/api/abc_tunebook_svg';\nimport extensionSvc from '../services/extensionSvc';\n\nconst rend"
  },
  {
    "path": "src/extensions/emojiExtension.js",
    "chars": 454,
    "preview": "import markdownItEmoji from 'markdown-it-emoji';\nimport extensionSvc from '../services/extensionSvc';\n\nextensionSvc.onGe"
  },
  {
    "path": "src/extensions/index.js",
    "chars": 138,
    "preview": "import './emojiExtension';\nimport './abcExtension';\nimport './katexExtension';\nimport './markdownExtension';\nimport './m"
  },
  {
    "path": "src/extensions/katexExtension.js",
    "chars": 1148,
    "preview": "import katex from 'katex';\nimport markdownItMath from './libs/markdownItMath';\nimport extensionSvc from '../services/ext"
  },
  {
    "path": "src/extensions/libs/markdownItAnchor.js",
    "chars": 1842,
    "preview": "export default (md) => {\n  md.core.ruler.before('replacements', 'anchors', (state) => {\n    const anchorHash = {};\n    l"
  },
  {
    "path": "src/extensions/libs/markdownItMath.js",
    "chars": 1809,
    "preview": "function texMath(state, silent) {\n  let startMathPos = state.pos;\n  if (state.src.charCodeAt(startMathPos) !== 0x24 /* $"
  },
  {
    "path": "src/extensions/libs/markdownItTasklist.js",
    "chars": 1420,
    "preview": "function attrSet(token, name, value) {\n  const index = token.attrIndex(name);\n  const attr = [name, value];\n\n  if (index"
  },
  {
    "path": "src/extensions/markdownExtension.js",
    "chars": 4581,
    "preview": "import Prism from 'prismjs';\nimport markdownitAbbr from 'markdown-it-abbr';\nimport markdownitDeflist from 'markdown-it-d"
  },
  {
    "path": "src/extensions/mermaidExtension.js",
    "chars": 1728,
    "preview": "import 'mermaid';\nimport extensionSvc from '../services/extensionSvc';\nimport utils from '../services/utils';\n\nconst con"
  },
  {
    "path": "src/icons/Alert.vue",
    "chars": 225,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M 13,14L 11,14L 11,9"
  },
  {
    "path": "src/icons/ArrowLeft.vue",
    "chars": 253,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M 20,11L 20,13L 7.98"
  },
  {
    "path": "src/icons/CheckCircle.vue",
    "chars": 366,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M 12,2C 17.5228,2 22"
  },
  {
    "path": "src/icons/Close.vue",
    "chars": 235,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M19,6.41L17.59,5L12,"
  },
  {
    "path": "src/icons/CodeBraces.vue",
    "chars": 640,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M 8,3C 6.89543,3 6,3"
  },
  {
    "path": "src/icons/CodeTags.vue",
    "chars": 246,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"2 2 20 20\">\n    <path d=\"M 14.6,16.6L 19.2,12"
  },
  {
    "path": "src/icons/ContentCopy.vue",
    "chars": 323,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M 19,21L 8,21L 8,7L "
  },
  {
    "path": "src/icons/ContentSave.vue",
    "chars": 316,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M15,9H5V5H15M12,19C1"
  },
  {
    "path": "src/icons/Database.vue",
    "chars": 415,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M12,3C7.58,3 4,4.79 "
  },
  {
    "path": "src/icons/Delete.vue",
    "chars": 217,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M19,4H15.5L14.5,3H9."
  },
  {
    "path": "src/icons/DotsHorizontal.vue",
    "chars": 494,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M 16,12C 16,10.8954 "
  },
  {
    "path": "src/icons/Download.vue",
    "chars": 327,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M 4.9994,19.9981L 18"
  },
  {
    "path": "src/icons/Eye.vue",
    "chars": 774,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M 11.9994,8.99813C 1"
  },
  {
    "path": "src/icons/FileImage.vue",
    "chars": 613,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M 12.9994,8.99807L 1"
  },
  {
    "path": "src/icons/FileMultiple.vue",
    "chars": 263,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"-2 -2 26 26\">\n    <path d=\"M15,7H20.5L15,1.5V"
  },
  {
    "path": "src/icons/FilePlus.vue",
    "chars": 264,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M13,9H18.5L13,3.5V9M"
  },
  {
    "path": "src/icons/Folder.vue",
    "chars": 229,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M10,4H4C2.89,4 2,4.8"
  },
  {
    "path": "src/icons/FolderMultiple.vue",
    "chars": 269,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M22,4H14L12,2H6C4.9,"
  },
  {
    "path": "src/icons/FolderPlus.vue",
    "chars": 269,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M10,4L12,6H20C21.1,6"
  },
  {
    "path": "src/icons/FormatBold.vue",
    "chars": 529,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M13.35,17.401l-4.201"
  },
  {
    "path": "src/icons/FormatItalic.vue",
    "chars": 272,
    "preview": "<template>\n\t<svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n\t\t<path d=\"M8.617,3.658l0,3.575l2."
  },
  {
    "path": "src/icons/FormatListBulleted.vue",
    "chars": 730,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M7.043,4.695l14.61,0"
  },
  {
    "path": "src/icons/FormatListChecks.vue",
    "chars": 240,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M3,5H9V11H3V5M5,7V9H"
  },
  {
    "path": "src/icons/FormatListNumbers.vue",
    "chars": 535,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M7.235,13.059l14.825"
  },
  {
    "path": "src/icons/FormatQuoteClose.vue",
    "chars": 302,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M14.446,18.235l2.92,"
  },
  {
    "path": "src/icons/FormatSize.vue",
    "chars": 307,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M2.007,12.526l3.156,"
  },
  {
    "path": "src/icons/FormatStrikethrough.vue",
    "chars": 623,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M20.874,12.059l0,1.7"
  },
  {
    "path": "src/icons/HelpCircle.vue",
    "chars": 903,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M 15.0661,11.2518L 1"
  },
  {
    "path": "src/icons/History.vue",
    "chars": 411,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M11,7V12.11L15.71,14"
  },
  {
    "path": "src/icons/Information.vue",
    "chars": 468,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M 12.9994,8.99805L 1"
  },
  {
    "path": "src/icons/Key.vue",
    "chars": 374,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M 7,14C 5.9,14 5,13."
  },
  {
    "path": "src/icons/LinkVariant.vue",
    "chars": 1206,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M 10.5858,13.4142C 1"
  },
  {
    "path": "src/icons/Login.vue",
    "chars": 598,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path fill=\"#000000\" fill-opa"
  },
  {
    "path": "src/icons/Logout.vue",
    "chars": 552,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path fill=\"#000000\" fill-opa"
  },
  {
    "path": "src/icons/Magnify.vue",
    "chars": 529,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path d=\"M 9.5,3C 13.0899,3 1"
  },
  {
    "path": "src/icons/Menu.vue",
    "chars": 300,
    "preview": "<template>\n  <svg xmlns=\"http://www.w3.org/2000/svg\" class=\"icon\" viewBox=\"0 0 24 24\">\n    <path fill=\"#000000\" fill-opa"
  }
]

// ... and 130 more files (download for full content)

About this extraction

This page contains the full source code of the benweet/stackedit GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 330 files (1005.7 KB), approximately 272.4k tokens, and a symbol index with 522 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!