The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .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
    ├── icon-128.png
    ├── icon-16.png
    ├── icon-256.png
    ├── icon-32.png
    ├── icon-512.png
    ├── icon-64.png
    └── manifest.json
├── config
    ├── dev.env.js
    ├── index.js
    └── prod.env.js
├── gulpfile.js
├── index.html
├── index.js
├── package-lock.json
├── package.json
├── server
    ├── conf.js
    ├── github.js
    ├── index.js
    ├── pandoc.js
    ├── pdf.js
    └── user.js
├── src
    ├── assets
    │   ├── favicon.png
    │   ├── fonts
    │   │   ├── RobotoMono-Bold.woff
    │   │   ├── RobotoMono-Regular.woff
    │   │   ├── lato-black-italic.woff
    │   │   ├── lato-black.woff
    │   │   ├── lato-normal-italic.woff
    │   │   └── lato-normal.woff
    │   ├── iconBlogger.svg
    │   ├── iconCouchdb.svg
    │   ├── iconDropbox.svg
    │   ├── iconGithub.svg
    │   ├── iconGitlab.svg
    │   ├── iconGoogle.svg
    │   ├── iconGoogleDrive.svg
    │   ├── iconGooglePhotos.svg
    │   ├── iconStackedit.svg
    │   ├── iconWordpress.svg
    │   ├── iconZendesk.svg
    │   └── logo.svg
    ├── 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
    │   ├── abc.png
    │   ├── discussion.png
    │   ├── favicon.ico
    │   ├── gfm.png
    │   ├── index.html
    │   ├── katex.gif
    │   ├── logo.svg
    │   ├── mermaid.gif
    │   ├── navigation-bar.png
    │   ├── providers.png
    │   ├── scroll-sync.gif
    │   ├── smart-layout.png
    │   ├── syntax-highlighting.gif
    │   ├── twemoji.png
    │   └── workspace.png
    ├── 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


/.babelrc:
--------------------------------------------------------------------------------
 1 | {
 2 |   "presets": [
 3 |     ["env", { "modules": false }],
 4 |     "stage-2"
 5 |   ],
 6 |   "plugins": ["transform-runtime"],
 7 |   "comments": false,
 8 |   "env": {
 9 |     "test": {
10 |       "presets": ["env", "stage-2"],
11 |       "plugins": ["transform-es2015-modules-commonjs", "dynamic-import-node"]
12 |     }
13 |   }
14 | }
15 | 


--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .git
3 | dist
4 | .history
5 | 


--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
 1 | root = true
 2 | 
 3 | [*]
 4 | charset = utf-8
 5 | indent_style = space
 6 | indent_size = 2
 7 | end_of_line = lf
 8 | insert_final_newline = true
 9 | trim_trailing_whitespace = true
10 | 


--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | build/*.js
2 | config/*.js
3 | src/libs/*.js
4 | 


--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
 1 | // http://eslint.org/docs/user-guide/configuring
 2 | 
 3 | module.exports = {
 4 |   root: true,
 5 |   parser: 'babel-eslint',
 6 |   parserOptions: {
 7 |     sourceType: 'module'
 8 |   },
 9 |   env: {
10 |     browser: true,
11 |   },
12 |   extends: 'airbnb-base',
13 |   // required to lint *.vue files
14 |   plugins: [
15 |     'html'
16 |   ],
17 |   globals: {
18 |     "NODE_ENV": false,
19 |     "VERSION": false
20 |   },
21 |   // check if imports actually resolve
22 |   'settings': {
23 |     'import/resolver': {
24 |       'webpack': {
25 |         'config': 'build/webpack.base.conf.js'
26 |       }
27 |     }
28 |   },
29 |   // add your custom rules here
30 |   'rules': {
31 |     'no-param-reassign': [2, { 'props': false }],
32 |     // don't require .vue extension when importing
33 |     'import/extensions': ['error', 'always', {
34 |       'js': 'never',
35 |       'vue': 'never'
36 |     }],
37 |     // allow optionalDependencies
38 |     'import/no-extraneous-dependencies': ['error', {
39 |       'optionalDependencies': ['test/unit/index.js']
40 |     }],
41 |     // allow debugger during development
42 |     'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
43 |   }
44 | }
45 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | .DS_Store
 2 | node_modules/
 3 | dist/
 4 | .history
 5 | .idea
 6 | npm-debug.log*
 7 | .vscode
 8 | stackedit_v4
 9 | chrome-app/*.zip
10 | /test/unit/coverage/
11 | 


--------------------------------------------------------------------------------
/.postcssrc.js:
--------------------------------------------------------------------------------
1 | // https://github.com/michael-ciniawsky/postcss-load-config
2 | 
3 | module.exports = {
4 |   "plugins": {
5 |     // to edit target browsers: use "browserlist" field in package.json
6 |     "autoprefixer": {}
7 |   }
8 | }
9 | 


--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 |   "processors": ["stylelint-processor-html"],
3 |   "extends": "stylelint-config-standard",
4 |   "rules": {
5 |     "no-empty-source": null
6 |   }
7 | }


--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
 1 | language: node_js
 2 | 
 3 | node_js:
 4 |   - "12"
 5 | 
 6 | services:
 7 |   - docker
 8 | 
 9 | before_deploy:
10 |   # Run docker build
11 |   - docker build -t benweet/stackedit .
12 |   # Install Helm
13 |   - curl -SL -o /tmp/get_helm.sh https://git.io/get_helm.sh
14 |   - chmod 700 /tmp/get_helm.sh
15 |   - /tmp/get_helm.sh
16 |   - helm init --client-only
17 | 
18 | deploy:
19 |   provider: script
20 |   script: bash build/deploy.sh
21 |   on:
22 |     tags: true
23 | 


--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
 1 | FROM benweet/stackedit-base
 2 | 
 3 | RUN mkdir -p /opt/stackedit
 4 | WORKDIR /opt/stackedit
 5 | 
 6 | COPY package*json /opt/stackedit/
 7 | COPY gulpfile.js /opt/stackedit/
 8 | RUN npm install --unsafe-perm \
 9 |   && npm cache clean --force
10 | COPY . /opt/stackedit
11 | ENV NODE_ENV production
12 | RUN npm run build
13 | 
14 | EXPOSE 8080
15 | 
16 | CMD [ "node", "." ]
17 | 


--------------------------------------------------------------------------------
/build/build.js:
--------------------------------------------------------------------------------
 1 | require('./check-versions')()
 2 | 
 3 | process.env.NODE_ENV = 'production'
 4 | 
 5 | var ora = require('ora')
 6 | var rm = require('rimraf')
 7 | var path = require('path')
 8 | var chalk = require('chalk')
 9 | var webpack = require('webpack')
10 | var config = require('../config')
11 | var webpackConfig = require('./webpack.prod.conf')
12 | 
13 | var spinner = ora('building for production...')
14 | spinner.start()
15 | 
16 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
17 |   if (err) throw err
18 |   webpack(webpackConfig, function (err, stats) {
19 |     spinner.stop()
20 |     if (err) throw err
21 |     process.stdout.write(stats.toString({
22 |       colors: true,
23 |       modules: false,
24 |       children: false,
25 |       chunks: false,
26 |       chunkModules: false
27 |     }) + '\n\n')
28 | 
29 |     console.log(chalk.cyan('  Build complete.\n'))
30 |     console.log(chalk.yellow(
31 |       '  Tip: built files are meant to be served over an HTTP server.\n' +
32 |       '  Opening index.html over file:// won\'t work.\n'
33 |     ))
34 |   })
35 | })
36 | 


--------------------------------------------------------------------------------
/build/check-versions.js:
--------------------------------------------------------------------------------
 1 | var chalk = require('chalk')
 2 | var semver = require('semver')
 3 | var packageConfig = require('../package.json')
 4 | var shell = require('shelljs')
 5 | function exec (cmd) {
 6 |   return require('child_process').execSync(cmd).toString().trim()
 7 | }
 8 | 
 9 | var versionRequirements = [
10 |   {
11 |     name: 'node',
12 |     currentVersion: semver.clean(process.version),
13 |     versionRequirement: packageConfig.engines.node
14 |   },
15 | ]
16 | 
17 | if (shell.which('npm')) {
18 |   versionRequirements.push({
19 |     name: 'npm',
20 |     currentVersion: exec('npm --version'),
21 |     versionRequirement: packageConfig.engines.npm
22 |   })
23 | }
24 | 
25 | module.exports = function () {
26 |   var warnings = []
27 |   for (var i = 0; i < versionRequirements.length; i++) {
28 |     var mod = versionRequirements[i]
29 |     if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
30 |       warnings.push(mod.name + ': ' +
31 |         chalk.red(mod.currentVersion) + ' should be ' +
32 |         chalk.green(mod.versionRequirement)
33 |       )
34 |     }
35 |   }
36 | 
37 |   if (warnings.length) {
38 |     console.log('')
39 |     console.log(chalk.yellow('To use this template, you must update following to modules:'))
40 |     console.log()
41 |     for (var i = 0; i < warnings.length; i++) {
42 |       var warning = warnings[i]
43 |       console.log('  ' + warning)
44 |     }
45 |     console.log()
46 |     process.exit(1)
47 |   }
48 | }
49 | 


--------------------------------------------------------------------------------
/build/deploy.sh:
--------------------------------------------------------------------------------
 1 | #!/bin/bash
 2 | set -e
 3 | 
 4 | # Tag and push docker image
 5 | docker login -u benweet -p "$DOCKER_PASSWORD"
 6 | docker tag benweet/stackedit "benweet/stackedit:$TRAVIS_TAG"
 7 | docker push benweet/stackedit:$TRAVIS_TAG
 8 | docker tag benweet/stackedit:$TRAVIS_TAG benweet/stackedit:latest
 9 | docker push benweet/stackedit:latest
10 | 
11 | # Build the chart
12 | cd "$TRAVIS_BUILD_DIR"
13 | npm run chart
14 | 
15 | # Add chart to helm repository
16 | git clone --branch master "https://benweet:$GITHUB_TOKEN@github.com/benweet/stackedit-charts.git" /tmp/charts
17 | cd /tmp/charts
18 | helm package "$TRAVIS_BUILD_DIR/dist/stackedit"
19 | helm repo index --url https://benweet.github.io/stackedit-charts/ .
20 | git config user.name "Benoit Schweblin"
21 | git config user.email "benoit.schweblin@gmail.com"
22 | git add .
23 | git commit -m "Added $TRAVIS_TAG"
24 | git push origin master
25 | 


--------------------------------------------------------------------------------
/build/dev-client.js:
--------------------------------------------------------------------------------
 1 | /* eslint-disable */
 2 | require('eventsource-polyfill')
 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
 4 | 
 5 | hotClient.subscribe(function (event) {
 6 |   if (event.action === 'reload') {
 7 |     window.location.reload()
 8 |   }
 9 | })
10 | 


--------------------------------------------------------------------------------
/build/utils.js:
--------------------------------------------------------------------------------
 1 | var path = require('path')
 2 | var config = require('../config')
 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
 4 | 
 5 | exports.assetsPath = function (_path) {
 6 |   var assetsSubDirectory = process.env.NODE_ENV === 'production'
 7 |     ? config.build.assetsSubDirectory
 8 |     : config.dev.assetsSubDirectory
 9 |   return path.posix.join(assetsSubDirectory, _path)
10 | }
11 | 
12 | exports.cssLoaders = function (options) {
13 |   options = options || {}
14 | 
15 |   var cssLoader = {
16 |     loader: 'css-loader',
17 |     options: {
18 |       minimize: process.env.NODE_ENV === 'production',
19 |       sourceMap: options.sourceMap
20 |     }
21 |   }
22 | 
23 |   // generate loader string to be used with extract text plugin
24 |   function generateLoaders (loader, loaderOptions) {
25 |     var loaders = [cssLoader]
26 |     if (loader) {
27 |       loaders.push({
28 |         loader: loader + '-loader',
29 |         options: Object.assign({}, loaderOptions, {
30 |           sourceMap: options.sourceMap
31 |         })
32 |       })
33 |     }
34 | 
35 |     // Extract CSS when that option is specified
36 |     // (which is the case during production build)
37 |     if (options.extract) {
38 |       return ExtractTextPlugin.extract({
39 |         use: loaders,
40 |         fallback: 'vue-style-loader'
41 |       })
42 |     } else {
43 |       return ['vue-style-loader'].concat(loaders)
44 |     }
45 |   }
46 | 
47 |   // https://vue-loader.vuejs.org/en/configurations/extract-css.html
48 |   return {
49 |     css: generateLoaders(),
50 |     postcss: generateLoaders(),
51 |     less: generateLoaders('less'),
52 |     sass: generateLoaders('sass', { indentedSyntax: true }),
53 |     scss: generateLoaders('sass'),
54 |     stylus: generateLoaders('stylus'),
55 |     styl: generateLoaders('stylus')
56 |   }
57 | }
58 | 
59 | // Generate loaders for standalone style files (outside of .vue)
60 | exports.styleLoaders = function (options) {
61 |   var output = []
62 |   var loaders = exports.cssLoaders(options)
63 |   for (var extension in loaders) {
64 |     var loader = loaders[extension]
65 |     output.push({
66 |       test: new RegExp('\\.' + extension + '
#39;),
67 |       use: loader
68 |     })
69 |   }
70 |   return output
71 | }
72 | 


--------------------------------------------------------------------------------
/build/vue-loader.conf.js:
--------------------------------------------------------------------------------
 1 | var utils = require('./utils')
 2 | var config = require('../config')
 3 | var isProduction = process.env.NODE_ENV === 'production'
 4 | 
 5 | module.exports = {
 6 |   loaders: utils.cssLoaders({
 7 |     sourceMap: isProduction
 8 |       ? config.build.productionSourceMap
 9 |       : config.dev.cssSourceMap,
10 |     extract: isProduction
11 |   })
12 | }
13 | 


--------------------------------------------------------------------------------
/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
 1 | var utils = require('./utils')
 2 | var webpack = require('webpack')
 3 | var config = require('../config')
 4 | var merge = require('webpack-merge')
 5 | var baseWebpackConfig = require('./webpack.base.conf')
 6 | var HtmlWebpackPlugin = require('html-webpack-plugin')
 7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
 8 | 
 9 | // add hot-reload related code to entry chunks
10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) {
11 |   baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
12 | })
13 | 
14 | module.exports = merge(baseWebpackConfig, {
15 |   module: {
16 |     rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
17 |   },
18 |   // cheap-module-eval-source-map is faster for development
19 |   devtool: 'source-map',
20 |   plugins: [
21 |     new webpack.DefinePlugin({
22 |       NODE_ENV: config.dev.env.NODE_ENV
23 |     }),
24 |     // https://github.com/glenjamin/webpack-hot-middleware#installation--usage
25 |     new webpack.HotModuleReplacementPlugin(),
26 |     new webpack.NoEmitOnErrorsPlugin(),
27 |     // https://github.com/ampedandwired/html-webpack-plugin
28 |     new HtmlWebpackPlugin({
29 |       filename: 'index.html',
30 |       template: 'index.html',
31 |       inject: true
32 |     }),
33 |     new FriendlyErrorsPlugin()
34 |   ]
35 | })
36 | 


--------------------------------------------------------------------------------
/build/webpack.style.conf.js:
--------------------------------------------------------------------------------
 1 | var path = require('path')
 2 | var utils = require('./utils')
 3 | var webpack = require('webpack')
 4 | var utils = require('./utils')
 5 | var config = require('../config')
 6 | var vueLoaderConfig = require('./vue-loader.conf')
 7 | var StylelintPlugin = require('stylelint-webpack-plugin')
 8 | var ExtractTextPlugin = require('extract-text-webpack-plugin')
 9 | var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
10 | 
11 | function resolve (dir) {
12 |   return path.join(__dirname, '..', dir)
13 | }
14 | 
15 | module.exports = {
16 |   entry: {
17 |     style: './src/styles/'
18 |   },
19 |   module: {
20 |     rules: [{
21 |       test: /\.(ttf|eot|otf|woff2?)(\?.*)?$/,
22 |       loader: 'file-loader',
23 |       options: {
24 |         name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
25 |       }
26 |     }]
27 |     .concat(utils.styleLoaders({
28 |       sourceMap: config.build.productionSourceMap,
29 |       extract: true
30 |     })),
31 |   },
32 |   output: {
33 |     path: config.build.assetsRoot,
34 |     filename: '[name].js',
35 |     publicPath: config.build.assetsPublicPath
36 |   },
37 |   plugins: [
38 |     new webpack.optimize.UglifyJsPlugin({
39 |       compress: {
40 |         warnings: false
41 |       },
42 |       sourceMap: true
43 |     }),
44 |     // extract css into its own file
45 |     new ExtractTextPlugin({
46 |       filename: '[name].css',
47 |     }),
48 |     // Compress extracted CSS. We are using this plugin so that possible
49 |     // duplicated CSS from different components can be deduped.
50 |     new OptimizeCSSPlugin({
51 |       cssProcessorOptions: {
52 |         safe: true
53 |       }
54 |     }),
55 |   ]
56 | }
57 | 


--------------------------------------------------------------------------------
/chart/.helmignore:
--------------------------------------------------------------------------------
 1 | # Patterns to ignore when building packages.
 2 | # This supports shell glob matching, relative path matching, and
 3 | # negation (prefixed with !). Only one pattern per line.
 4 | .DS_Store
 5 | # Common VCS dirs
 6 | .git/
 7 | .gitignore
 8 | .bzr/
 9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *~
18 | # Various IDEs
19 | .project
20 | .idea/
21 | *.tmproj
22 | .vscode/
23 | 


--------------------------------------------------------------------------------
/chart/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | appVersion: vSTACKEDIT_VERSION
3 | description: In-browser Markdown editor
4 | name: stackedit
5 | version: STACKEDIT_VERSION
6 | 


--------------------------------------------------------------------------------
/chart/templates/NOTES.txt:
--------------------------------------------------------------------------------
 1 | 1. Get the application URL by running these commands:
 2 | {{- if .Values.ingress.enabled }}
 3 | {{- range $host := .Values.ingress.hosts }}
 4 |   {{- range .paths }}
 5 |   http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }}
 6 |   {{- end }}
 7 | {{- end }}
 8 | {{- else if contains "NodePort" .Values.service.type }}
 9 |   export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "stackedit.fullname" . }})
10 |   export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
11 |   echo http://$NODE_IP:$NODE_PORT
12 | {{- else if contains "LoadBalancer" .Values.service.type }}
13 |      NOTE: It may take a few minutes for the LoadBalancer IP to be available.
14 |            You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "stackedit.fullname" . }}'
15 |   export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "stackedit.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
16 |   echo http://$SERVICE_IP:{{ .Values.service.port }}
17 | {{- else if contains "ClusterIP" .Values.service.type }}
18 |   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}")
19 |   echo "Visit http://127.0.0.1:8080 to use your application"
20 |   kubectl port-forward $POD_NAME 8080:80
21 | {{- end }}
22 | 


--------------------------------------------------------------------------------
/chart/templates/_helpers.tpl:
--------------------------------------------------------------------------------
 1 | {{/* vim: set filetype=mustache: */}}
 2 | {{/*
 3 | Expand the name of the chart.
 4 | */}}
 5 | {{- define "stackedit.name" -}}
 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
 7 | {{- end -}}
 8 | 
 9 | {{/*
10 | Create a default fully qualified app name.
11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
12 | If release name contains chart name it will be used as a full name.
13 | */}}
14 | {{- define "stackedit.fullname" -}}
15 | {{- if .Values.fullnameOverride -}}
16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
17 | {{- else -}}
18 | {{- $name := default .Chart.Name .Values.nameOverride -}}
19 | {{- if contains $name .Release.Name -}}
20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}}
21 | {{- else -}}
22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
23 | {{- end -}}
24 | {{- end -}}
25 | {{- end -}}
26 | 
27 | {{/*
28 | Create chart name and version as used by the chart label.
29 | */}}
30 | {{- define "stackedit.chart" -}}
31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
32 | {{- end -}}
33 | 
34 | {{/*
35 | Common labels
36 | */}}
37 | {{- define "stackedit.labels" -}}
38 | app.kubernetes.io/name: {{ include "stackedit.name" . }}
39 | helm.sh/chart: {{ include "stackedit.chart" . }}
40 | app.kubernetes.io/instance: {{ .Release.Name }}
41 | {{- if .Chart.AppVersion }}
42 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
43 | {{- end }}
44 | app.kubernetes.io/managed-by: {{ .Release.Service }}
45 | {{- end -}}
46 | 


--------------------------------------------------------------------------------
/chart/templates/ingress.yaml:
--------------------------------------------------------------------------------
 1 | {{- if .Values.ingress.enabled -}}
 2 | {{- $fullName := include "stackedit.fullname" . -}}
 3 | apiVersion: networking.k8s.io/v1
 4 | kind: Ingress
 5 | metadata:
 6 |   name: {{ $fullName }}
 7 |   labels:
 8 | {{ include "stackedit.labels" . | indent 4 }}
 9 |   {{- with .Values.ingress.annotations }}
10 |   annotations:
11 |     {{- toYaml . | nindent 4 }}
12 |   {{- end }}
13 | spec:
14 | {{- if .Values.ingress.tls }}
15 |   tls:
16 |   {{- range .Values.ingress.tls }}
17 |     - hosts:
18 |       {{- range .hosts }}
19 |         - {{ . | quote }}
20 |       {{- end }}
21 |       secretName: {{ .secretName }}
22 |   {{- end }}
23 | {{- end }}
24 |   rules:
25 |   {{- range .Values.ingress.hosts }}
26 |     - host: {{ .host | quote }}
27 |       http:
28 |         paths:
29 |         {{- range .paths }}
30 |           - path: {{ . }}
31 |             pathType: Prefix
32 |             backend:
33 |               service:
34 |                 name: {{ $fullName }}
35 |                 port:
36 |                   name: http
37 |         {{- end }}
38 |   {{- end }}
39 | {{- end }}
40 | 


--------------------------------------------------------------------------------
/chart/templates/service.yaml:
--------------------------------------------------------------------------------
 1 | apiVersion: v1
 2 | kind: Service
 3 | metadata:
 4 |   name: {{ include "stackedit.fullname" . }}
 5 |   labels:
 6 | {{ include "stackedit.labels" . | indent 4 }}
 7 | spec:
 8 |   type: {{ .Values.service.type }}
 9 |   ports:
10 |     - port: {{ .Values.service.port }}
11 |       targetPort: http
12 |       protocol: TCP
13 |       name: http
14 |   selector:
15 |     app.kubernetes.io/name: {{ include "stackedit.name" . }}
16 |     app.kubernetes.io/instance: {{ .Release.Name }}
17 | 


--------------------------------------------------------------------------------
/chart/templates/tests/test-connection.yaml:
--------------------------------------------------------------------------------
 1 | apiVersion: v1
 2 | kind: Pod
 3 | metadata:
 4 |   name: "{{ include "stackedit.fullname" . }}-test-connection"
 5 |   labels:
 6 | {{ include "stackedit.labels" . | indent 4 }}
 7 |   annotations:
 8 |     "helm.sh/hook": test-success
 9 | spec:
10 |   containers:
11 |     - name: wget
12 |       image: busybox
13 |       command: ['wget']
14 |       args:  ['{{ include "stackedit.fullname" . }}:{{ .Values.service.port }}']
15 |   restartPolicy: Never
16 | 


--------------------------------------------------------------------------------
/chart/values.yaml:
--------------------------------------------------------------------------------
 1 | # Default values for stackedit.
 2 | # This is a YAML-formatted file.
 3 | # Declare variables to be passed into your templates.
 4 | 
 5 | dropboxAppKey: ""
 6 | dropboxAppKeyFull: ""
 7 | googleClientId: ""
 8 | googleApiKey: ""
 9 | githubClientId: ""
10 | githubClientSecret: ""
11 | wordpressClientId: ""
12 | wordpressSecret: ""
13 | paypalReceiverEmail: ""
14 | awsAccessKeyId: ""
15 | awsSecretAccessKey: ""
16 | 
17 | replicaCount: 1
18 | 
19 | image:
20 |   repository: benweet/stackedit
21 |   tag: vSTACKEDIT_VERSION
22 |   pullPolicy: IfNotPresent
23 | 
24 | imagePullSecrets: []
25 | nameOverride: ""
26 | fullnameOverride: ""
27 | 
28 | service:
29 |   type: ClusterIP
30 |   port: 80
31 | 
32 | ingress:
33 |   enabled: false
34 |   annotations:
35 |    # kubernetes.io/ingress.class: nginx
36 |    # certmanager.k8s.io/issuer: letsencrypt-prod
37 |    # certmanager.k8s.io/acme-challenge-type: http01
38 |   hosts: []
39 |    # - host: stackedit.example.com
40 |    #   paths:
41 |    #     - /
42 | 
43 |   tls: []
44 |    # - secretName: stackedit-tls
45 |    #   hosts:
46 |    #     - stackedit.example.com
47 | 
48 | resources: {}
49 |   # We usually recommend not to specify default resources and to leave this as a conscious
50 |   # choice for the user. This also increases chances charts run on environments with little
51 |   # resources, such as Minikube. If you do want to specify resources, uncomment the following
52 |   # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
53 |   # limits:
54 |   #   cpu: 100m
55 |   #   memory: 128Mi
56 |   # requests:
57 |   #   cpu: 100m
58 |   #   memory: 128Mi
59 | 
60 | nodeSelector: {}
61 | 
62 | tolerations: []
63 | 
64 | affinity: {}
65 | 


--------------------------------------------------------------------------------
/chrome-app/icon-128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/chrome-app/icon-128.png


--------------------------------------------------------------------------------
/chrome-app/icon-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/chrome-app/icon-16.png


--------------------------------------------------------------------------------
/chrome-app/icon-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/chrome-app/icon-256.png


--------------------------------------------------------------------------------
/chrome-app/icon-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/chrome-app/icon-32.png


--------------------------------------------------------------------------------
/chrome-app/icon-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/chrome-app/icon-512.png


--------------------------------------------------------------------------------
/chrome-app/icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/chrome-app/icon-64.png


--------------------------------------------------------------------------------
/chrome-app/manifest.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "StackEdit",
 3 |   "description": "In-browser Markdown editor",
 4 |   "version": "1.0.13",
 5 |   "manifest_version": 2,
 6 |   "container" : "GOOGLE_DRIVE",
 7 |   "api_console_project_id" : "241271498917",
 8 |   "icons": {
 9 |     "16": "icon-16.png",
10 |     "32": "icon-32.png",
11 |     "64": "icon-64.png",
12 |     "128": "icon-128.png",
13 |     "256": "icon-256.png",
14 |     "512": "icon-512.png"
15 |   },
16 |   "app": {
17 |     "urls": [
18 |       "https://stackedit.io/"
19 |     ],
20 |     "launch": {
21 |       "web_url": "https://stackedit.io/app"
22 |     }
23 |   },
24 |   "offline_enabled": true,
25 |   "permissions": [
26 |     "unlimitedStorage"
27 |   ]
28 | }
29 | 


--------------------------------------------------------------------------------
/config/dev.env.js:
--------------------------------------------------------------------------------
1 | var merge = require('webpack-merge')
2 | var prodEnv = require('./prod.env')
3 | 
4 | module.exports = merge(prodEnv, {
5 |   NODE_ENV: '"development"'
6 | })
7 | 


--------------------------------------------------------------------------------
/config/index.js:
--------------------------------------------------------------------------------
 1 | // see http://vuejs-templates.github.io/webpack for documentation.
 2 | var path = require('path')
 3 | 
 4 | module.exports = {
 5 |   build: {
 6 |     env: require('./prod.env'),
 7 |     index: path.resolve(__dirname, '../dist/index.html'),
 8 |     assetsRoot: path.resolve(__dirname, '../dist'),
 9 |     assetsSubDirectory: 'static',
10 |     assetsPublicPath: '/',
11 |     productionSourceMap: true,
12 |     // Gzip off by default as many popular static hosts such as
13 |     // Surge or Netlify already gzip all static assets for you.
14 |     // Before setting to `true`, make sure to:
15 |     // npm install --save-dev compression-webpack-plugin
16 |     productionGzip: false,
17 |     productionGzipExtensions: ['js', 'css'],
18 |     // Run the build command with an extra argument to
19 |     // View the bundle analyzer report after build finishes:
20 |     // `npm run build --report`
21 |     // Set to `true` or `false` to always turn it on or off
22 |     bundleAnalyzerReport: process.env.npm_config_report
23 |   },
24 |   dev: {
25 |     env: require('./dev.env'),
26 |     port: 8080,
27 |     autoOpenBrowser: false,
28 |     assetsSubDirectory: 'static',
29 |     assetsPublicPath: '/',
30 |     proxyTable: {},
31 |     // CSS Sourcemaps off by default because relative paths are "buggy"
32 |     // with this option, according to the CSS-Loader README
33 |     // (https://github.com/webpack/css-loader#sourcemaps)
34 |     // In our experience, they generally work as expected,
35 |     // just be aware of this issue when enabling this option.
36 |     // cssSourceMap: false
37 |     cssSourceMap: true
38 |   }
39 | }
40 | 


--------------------------------------------------------------------------------
/config/prod.env.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 |   NODE_ENV: '"production"'
3 | }
4 | 


--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
 1 | const path = require('path');
 2 | const gulp = require('gulp');
 3 | const concat = require('gulp-concat');
 4 | 
 5 | const prismScripts = [
 6 |   'prismjs/components/prism-core',
 7 |   'prismjs/components/prism-markup',
 8 |   'prismjs/components/prism-clike',
 9 |   'prismjs/components/prism-c',
10 |   'prismjs/components/prism-javascript',
11 |   'prismjs/components/prism-css',
12 |   'prismjs/components/prism-ruby',
13 |   'prismjs/components/prism-cpp',
14 | ].map(require.resolve);
15 | prismScripts.push(
16 |   path.join(path.dirname(require.resolve('prismjs/components/prism-core')), 'prism-!(*.min).js'));
17 | 
18 | gulp.task('build-prism', () => gulp.src(prismScripts)
19 |   .pipe(concat('prism.js'))
20 |   .pipe(gulp.dest(path.dirname(require.resolve('prismjs')))));
21 | 


--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html>
 3 |   <head>
 4 |     <meta charset="utf-8">
 5 |     <title>StackEdit</title>
 6 |     <link rel="canonical" href="https://stackedit.io/app">
 7 |     <meta name="description" content="Free, open-source, full-featured Markdown editor.">
 8 |     <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1">
 9 |     <meta name="mobile-web-app-capable" content="yes">
10 |     <meta name="apple-mobile-web-app-capable" content="yes">
11 |     <meta name="apple-mobile-web-app-status-bar-style" content="black">
12 |   </head>
13 |   <body>
14 |     <div id="app"></div>
15 |     <!-- built files will be auto injected -->
16 |   </body>
17 | </html>
18 | 


--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
 1 | const env = require('./config/prod.env');
 2 | 
 3 | Object.keys(env).forEach((key) => {
 4 |   if (!process.env[key]) {
 5 |     process.env[key] = JSON.parse(env[key]);
 6 |   }
 7 | });
 8 | 
 9 | const http = require('http');
10 | const express = require('express');
11 | 
12 | const app = express();
13 | 
14 | require('./server')(app);
15 | 
16 | const port = parseInt(process.env.PORT || 8080, 10);
17 | const httpServer = http.createServer(app);
18 | httpServer.listen(port, null, () => {
19 |   console.log(`HTTP server started: http://localhost:${port}`);
20 | });
21 | 
22 | // Handle graceful shutdown
23 | process.on('SIGTERM', () => {
24 |   httpServer.close(() => {
25 |     process.exit(0);
26 |   });
27 | });
28 | 


--------------------------------------------------------------------------------
/server/conf.js:
--------------------------------------------------------------------------------
 1 | const pandocPath = process.env.PANDOC_PATH || 'pandoc';
 2 | const wkhtmltopdfPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
 3 | const userBucketName = process.env.USER_BUCKET_NAME || 'stackedit-users';
 4 | const paypalUri = process.env.PAYPAL_URI || 'https://www.paypal.com/cgi-bin/webscr';
 5 | const paypalReceiverEmail = process.env.PAYPAL_RECEIVER_EMAIL;
 6 | 
 7 | const dropboxAppKey = process.env.DROPBOX_APP_KEY;
 8 | const dropboxAppKeyFull = process.env.DROPBOX_APP_KEY_FULL;
 9 | const githubClientId = process.env.GITHUB_CLIENT_ID;
10 | const githubClientSecret = process.env.GITHUB_CLIENT_SECRET;
11 | const googleClientId = process.env.GOOGLE_CLIENT_ID;
12 | const googleApiKey = process.env.GOOGLE_API_KEY;
13 | const wordpressClientId = process.env.WORDPRESS_CLIENT_ID;
14 | 
15 | exports.values = {
16 |   pandocPath,
17 |   wkhtmltopdfPath,
18 |   userBucketName,
19 |   paypalUri,
20 |   paypalReceiverEmail,
21 |   dropboxAppKey,
22 |   dropboxAppKeyFull,
23 |   githubClientId,
24 |   githubClientSecret,
25 |   googleClientId,
26 |   googleApiKey,
27 |   wordpressClientId,
28 | };
29 | 
30 | exports.publicValues = {
31 |   dropboxAppKey,
32 |   dropboxAppKeyFull,
33 |   githubClientId,
34 |   googleClientId,
35 |   googleApiKey,
36 |   wordpressClientId,
37 |   allowSponsorship: !!paypalReceiverEmail,
38 | };
39 | 


--------------------------------------------------------------------------------
/server/github.js:
--------------------------------------------------------------------------------
 1 | const qs = require('qs'); // eslint-disable-line import/no-extraneous-dependencies
 2 | const request = require('request');
 3 | const conf = require('./conf');
 4 | 
 5 | function githubToken(clientId, code) {
 6 |   return new Promise((resolve, reject) => {
 7 |     request({
 8 |       method: 'POST',
 9 |       url: 'https://github.com/login/oauth/access_token',
10 |       qs: {
11 |         client_id: clientId,
12 |         client_secret: conf.values.githubClientSecret,
13 |         code,
14 |       },
15 |     }, (err, res, body) => {
16 |       if (err) {
17 |         reject(err);
18 |       }
19 |       const token = qs.parse(body).access_token;
20 |       if (token) {
21 |         resolve(token);
22 |       } else {
23 |         reject(res.statusCode);
24 |       }
25 |     });
26 |   });
27 | }
28 | 
29 | exports.githubToken = (req, res) => {
30 |   githubToken(req.query.clientId, req.query.code)
31 |     .then(
32 |       token => res.send(token),
33 |       err => res
34 |         .status(400)
35 |         .send(err ? err.message || err.toString() : 'bad_code'),
36 |     );
37 | };
38 | 


--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
 1 | const compression = require('compression');
 2 | const serveStatic = require('serve-static');
 3 | const bodyParser = require('body-parser');
 4 | const path = require('path');
 5 | const user = require('./user');
 6 | const github = require('./github');
 7 | const pdf = require('./pdf');
 8 | const pandoc = require('./pandoc');
 9 | const conf = require('./conf');
10 | 
11 | const resolvePath = pathToResolve => path.join(__dirname, '..', pathToResolve);
12 | 
13 | module.exports = (app) => {
14 |   if (process.env.NODE_ENV === 'production') {
15 |     // Enable CORS for fonts
16 |     app.all('*', (req, res, next) => {
17 |       if (/\.(eot|ttf|woff2?|svg)$/.test(req.url)) {
18 |         res.header('Access-Control-Allow-Origin', '*');
19 |       }
20 |       next();
21 |     });
22 | 
23 |     // Use gzip compression
24 |     app.use(compression());
25 |   }
26 | 
27 |   app.get('/oauth2/githubToken', github.githubToken);
28 |   app.get('/conf', (req, res) => res.send(conf.publicValues));
29 |   app.get('/userInfo', user.userInfo);
30 |   app.post('/pdfExport', pdf.generate);
31 |   app.post('/pandocExport', pandoc.generate);
32 |   app.post('/paypalIpn', bodyParser.urlencoded({
33 |     extended: false,
34 |   }), user.paypalIpn);
35 | 
36 |   // Serve landing.html
37 |   app.get('/', (req, res) => res.sendFile(resolvePath('static/landing/index.html')));
38 |   // Serve sitemap.xml
39 |   app.get('/sitemap.xml', (req, res) => res.sendFile(resolvePath('static/sitemap.xml')));
40 |   // Serve callback.html
41 |   app.get('/oauth2/callback', (req, res) => res.sendFile(resolvePath('static/oauth2/callback.html')));
42 |   // Google Drive action receiver
43 |   app.get('/googleDriveAction', (req, res) =>
44 |     res.redirect(`./app#providerId=googleDrive&state=${encodeURIComponent(req.query.state)}`));
45 | 
46 |   // Serve static resources
47 |   if (process.env.NODE_ENV === 'production') {
48 |     // Serve index.html in /app
49 |     app.get('/app', (req, res) => res.sendFile(resolvePath('dist/index.html')));
50 | 
51 |     // Serve style.css with 1 day max-age
52 |     app.get('/style.css', (req, res) => res.sendFile(resolvePath('dist/style.css'), {
53 |       maxAge: '1d',
54 |     }));
55 | 
56 |     // Serve the static folder with 1 year max-age
57 |     app.use('/static', serveStatic(resolvePath('dist/static'), {
58 |       maxAge: '1y',
59 |     }));
60 | 
61 |     app.use(serveStatic(resolvePath('dist')));
62 |   }
63 | };
64 | 


--------------------------------------------------------------------------------
/src/assets/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/src/assets/favicon.png


--------------------------------------------------------------------------------
/src/assets/fonts/RobotoMono-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/src/assets/fonts/RobotoMono-Bold.woff


--------------------------------------------------------------------------------
/src/assets/fonts/RobotoMono-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/src/assets/fonts/RobotoMono-Regular.woff


--------------------------------------------------------------------------------
/src/assets/fonts/lato-black-italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/src/assets/fonts/lato-black-italic.woff


--------------------------------------------------------------------------------
/src/assets/fonts/lato-black.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/src/assets/fonts/lato-black.woff


--------------------------------------------------------------------------------
/src/assets/fonts/lato-normal-italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/src/assets/fonts/lato-normal-italic.woff


--------------------------------------------------------------------------------
/src/assets/fonts/lato-normal.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/src/assets/fonts/lato-normal.woff


--------------------------------------------------------------------------------
/src/assets/iconBlogger.svg:
--------------------------------------------------------------------------------
 1 | 
 2 | <svg
 3 |   xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 180">
 4 |   <g>
 5 |     <path d="M20.512,178.499c-3.359,-0.884 -6.258,-2.184 -8.931,-4.006c-2.257,-1.538 -5.556,-4.717 -6.811,-6.563c-1.532,-2.255 -3.293,-6.117 -4.011,-8.795c-0.732,-2.732 -0.743,-3.82 -0.757,-69.395c-0.013,-65.245 0.002,-66.679 0.72,-69.483c2.537,-9.916 10.395,-17.46 20.529,-19.711c2.914,-0.647 133.08,-0.76 136.223,-0.118c8.509,1.738 15.198,6.846 19.068,14.564c3.078,6.135 2.803,-0.617 2.943,72.231c0.09,46.349 0.007,65.808 -0.288,68.232c-1.386,11.345 -9.211,20.143 -20.471,23.019c-2.88,0.735 -3.882,0.746 -69.275,0.726c-63.227,-0.019 -66.474,-0.052 -68.939,-0.701l0,0Z" style="fill:#f57d00;fill-rule:nonzero;"/>
 6 |     <path d="M115.162,144.835c8.064,-1.1 14.384,-4.333 20.313,-10.39c4.289,-4.382 6.974,-9.125 8.728,-15.419c0.729,-2.615 0.79,-3.888 0.924,-19.242c0.101,-11.588 0.017,-17.015 -0.285,-18.385c-0.437,-1.986 -1.677,-3.83 -3.092,-4.599c-0.435,-0.237 -3.224,-0.538 -6.198,-0.67c-4.982,-0.221 -5.54,-0.318 -7.113,-1.24c-2.494,-1.462 -3.181,-3.041 -3.188,-7.327c-0.013,-8.189 -3.421,-15.792 -10.155,-22.654c-4.797,-4.889 -10.149,-8.198 -16.257,-10.052c-1.462,-0.444 -4.736,-0.595 -15.702,-0.725c-17.207,-0.203 -21.026,0.15 -26.884,2.483c-10.8,4.302 -18.56,13.368 -21.39,24.99c-0.532,2.183 -0.635,5.682 -0.761,25.779c-0.157,25.177 0.016,28.874 1.59,33.864c1.299,4.122 2.611,6.648 5.313,10.234c5.146,6.83 12.86,11.763 20.572,13.156c3.67,0.663 48.948,0.829 53.585,0.197Z" style="fill:#fff;fill-rule:nonzero;"/>
 7 |     <path d="M67.575,75.717c-4.123,-1.136 -5.663,-7.051 -2.633,-10.111c1.937,-1.955 2.472,-2.029 14.595,-2.029c10.883,0 11.249,0.023 12.848,0.831c2.31,1.167 3.314,2.812 3.314,5.432c0,2.367 -0.943,4.025 -3.046,5.357c-1.129,0.716 -1.804,0.76 -12.467,0.823c-6.584,0.039 -11.83,-0.087 -12.611,-0.303l0,0Z" style="fill:#f57d00;fill-rule:nonzero;"/>
 8 |     <path d="M67.058,115.526c-1.769,-0.771 -3.417,-2.913 -3.702,-4.813c-0.272,-1.809 0.638,-4.296 2.032,-5.558c1.757,-1.59 2.528,-1.643 24.134,-1.66c22.227,-0.017 22.111,-0.027 24.219,1.941c2.976,2.78 2.349,7.728 -1.239,9.76l-3.686,0.6l-19.213,0.224c-16.883,0.198 -21.666,-0.111 -22.545,-0.494l0,0Z" style="fill:#f57d00;fill-rule:nonzero;"/>
 9 |   </g>
10 | </svg>
11 | 


--------------------------------------------------------------------------------
/src/assets/iconCouchdb.svg:
--------------------------------------------------------------------------------
1 | <svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1"
2 |   xmlns="http://www.w3.org/2000/svg">
3 |   <path d="M405.365,303.996c0,20.375 -11.207,30.563 -31.582,31.582l-248.584,0c-20.376,0 -31.583,-10.188 -31.583,-31.582c0,-20.376 11.207,-30.564 31.583,-31.583l249.602,0c20.376,1.019 30.564,11.207 30.564,31.583Zm-30.564,46.864l-249.602,0c-20.376,0 -31.583,10.188 -31.583,31.582c0,20.376 11.207,30.564 31.583,31.583l249.602,0c20.376,0 31.583,-10.188 31.583,-31.583c-1.019,-21.394 -11.207,-31.582 -31.583,-31.582Zm77.428,-172.175c-20.376,0 -31.582,10.188 -31.582,30.563l0,172.175c0,20.376 11.206,30.564 31.582,31.583c30.564,-1.019 46.864,-31.583 46.864,-93.729l0,-77.427c0,-41.771 -16.3,-62.146 -46.864,-63.165Zm-404.458,0c-30.564,1.019 -46.864,21.394 -46.864,63.165l0,77.427c0,62.146 16.3,92.71 46.864,93.729c20.376,0 31.582,-10.188 31.582,-31.583l0,-171.156c-1.019,-20.375 -11.206,-30.563 -31.582,-31.582Zm404.458,-15.282c0,-51.958 -27.507,-76.409 -77.428,-77.428l-249.602,0c-50.94,1.019 -77.428,26.489 -77.428,77.428c30.563,0 46.864,16.301 46.864,46.864c0,30.564 16.301,46.864 46.864,46.864l218.021,0c30.563,0 46.864,-16.3 46.864,-46.864c-1.019,-31.582 16.3,-45.845 45.845,-46.864Z" style="fill:#e42528;fill-rule:nonzero;"/>
4 | </svg>
5 | 


--------------------------------------------------------------------------------
/src/assets/iconDropbox.svg:
--------------------------------------------------------------------------------
 1 | 
 2 | <svg
 3 |   xmlns="http://www.w3.org/2000/svg" viewBox="0 0 42.4 39.5">
 4 |   <g fill="#007EE5">
 5 |     <path d="M12.5 0L0 8.1l8.7 7 12.5-7.8"/>
 6 |     <path d="M0 21.9l12.5 8.2 8.7-7.3-12.5-7.7m12.5 7.7l8.8 7.3L42.4 22l-8.6-6.9m8.6-7L30 0l-8.8 7.3 12.6 7.8"/>
 7 |     <path d="M21.3 24.4l-8.8 7.3-3.7-2.5V32l12.5 7.5L33.8 32v-2.8L30 31.7"/>
 8 |   </g>
 9 | </svg>
10 | 


--------------------------------------------------------------------------------
/src/assets/iconGithub.svg:
--------------------------------------------------------------------------------
1 | 
2 | <svg
3 |   xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 58">
4 |   <g fill="none" fill-rule="evenodd">
5 |     <path d="m1324.62 140c-16.355 0-29.616 13.219-29.616 29.527 0 13.04 8.485 24.11 20.256 28.01 1.482.27 2.02-.642 2.02-1.425 0-.7-.025-2.557-.04-5.02-8.238 1.784-9.976-3.958-9.976-3.958-1.347-3.411-3.289-4.317-3.289-4.317-2.689-1.832.204-1.796.204-1.796 2.973.21 4.536 3.043 4.536 3.043 2.642 4.511 6.931 3.208 8.62 2.454.269-1.909 1.033-3.21 1.88-3.948-6.576-.745-13.491-3.279-13.491-14.592 0-3.223 1.155-5.858 3.049-7.922-.305-.747-1.322-3.748.289-7.814 0 0 2.487-.794 8.145 3.03 2.362-.656 4.896-.982 7.415-.995 2.515.013 5.05.339 7.415.995 5.655-3.821 8.136-3.03 8.136-3.03 1.616 4.065.6 7.07.295 7.814 1.898 2.064 3.045 4.7 3.045 7.922 0 11.343-6.925 13.838-13.524 14.569 1.064.912 2.01 2.713 2.01 5.468 0 3.946-.036 7.13-.036 8.098 0 .79.533 1.709 2.036 1.421 11.758-3.913 20.238-14.971 20.238-28.01 0-16.309-13.262-29.527-29.62-29.527" transform="translate(-1295-140)" fill="#181616"/>
6 |   </g>
7 | </svg>
8 | 


--------------------------------------------------------------------------------
/src/assets/iconGitlab.svg:
--------------------------------------------------------------------------------
 1 | 
 2 | <svg width="100%" height="100%" viewBox="0 0 30 30" version="1.1"
 3 |   xmlns="http://www.w3.org/2000/svg"
 4 |   style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421;">
 5 |   <path d="M14.581,28.019l5.369,-16.526l-10.738,0l5.369,16.526l0,0Z" style="fill:#e24329;"/>
 6 |   <path d="M14.581,28.019l-5.37,-16.526l-7.525,0l12.895,16.526l0,0Z" style="fill:#fc6d26;"/>
 7 |   <path d="M1.686,11.493l-1.632,5.022c-0.148,0.458 0.015,0.96 0.404,1.243l14.123,10.261l-12.895,-16.526l0,0Z" style="fill:#fca326;"/>
 8 |   <path d="M1.686,11.493l7.526,0l-3.235,-9.953c-0.166,-0.512 -0.89,-0.512 -1.057,0l-3.234,9.953l0,0Z" style="fill:#e24329;"/>
 9 |   <path d="M14.581,28.019l5.369,-16.526l7.526,0l-12.895,16.526l0,0Z" style="fill:#fc6d26;"/>
10 |   <path d="M27.476,11.493l1.631,5.022c0.149,0.458 -0.014,0.96 -0.404,1.243l-14.122,10.261l12.895,-16.526l0,0Z" style="fill:#fca326;"/>
11 |   <path d="M27.476,11.493l-7.526,0l3.234,-9.953c0.167,-0.512 0.891,-0.512 1.058,0l3.234,9.953l0,0Z" style="fill:#e24329;"/>
12 | </svg>
13 | 


--------------------------------------------------------------------------------
/src/assets/iconGoogle.svg:
--------------------------------------------------------------------------------
 1 | <svg xmlns="http://www.w3.org/2000/svg"
 2 |      xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48">
 3 |   <defs>
 4 |     <path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"/>
 5 |   </defs>
 6 |   <clipPath id="b">
 7 |     <use xlink:href="#a" overflow="visible"/>
 8 |   </clipPath>
 9 |   <path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"/>
10 |   <path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"/>
11 |   <path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"/>
12 |   <path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"/>
13 | </svg>
14 | 


--------------------------------------------------------------------------------
/src/assets/iconGoogleDrive.svg:
--------------------------------------------------------------------------------
1 | <svg
2 |   xmlns="http://www.w3.org/2000/svg" viewBox="0 0 133156 115341">
3 |   <g>
4 |     <polygon style="fill:#3777E3" points="22194,115341 44385,76894 133156,76894 110963,115341 "/>
5 |     <polygon style="fill:#FFCF63" points="88772,76894 133156,76894 88772,0 44385,0 "/>
6 |     <polygon style="fill:#11A861" points="0,76894 22194,115341 66578,38447 44385,0 "/>
7 |   </g>
8 | </svg>
9 | 


--------------------------------------------------------------------------------
/src/assets/iconGooglePhotos.svg:
--------------------------------------------------------------------------------
 1 | 
 2 | <svg
 3 |   xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 511">
 4 |   <path d="M255.912,0.08c1.4,0.8 2.6,2 3.7,3.2c41.3,41.5 82.7,83 123.899,124.6c-26,25.6 -51.6,51.6 -77.399,77.3c-9.7,9.8 -19.601,19.4 -29.2,29.4c-7.2,-17.4 -14.1,-34.9 -21,-52.4c0,-18.2 0.1,-36.4 0,-54.7c-0.1,-42.4 -0.2,-84.9 0,-127.4l0,0Z" style="fill:#dc4b3e;fill-rule:nonzero;stroke:#dd4b39;stroke-width:0.09px;"/>
 5 |   <path d="M127.812,127.48l128.1,0c0.1,18.3 0,36.5 0,54.7c-7.1,17.2 -14,34.5 -20.8,51.9c-2.2,-1.2 -3.8,-3 -5.5,-4.8l-101.4,-101.4l-0.4,-0.4Z" style="fill:#ff9e0e;fill-rule:nonzero;stroke:#ef851c;stroke-width:0.09px;"/>
 6 |   <path d="M383.511,127.88l0.4,-0.3c-0.1,42.6 -0.1,85.3 0,127.9l-55.1,0c-17.2,-7.2 -34.601,-13.8 -51.9,-20.9c9.6,-10 19.5,-19.6 29.2,-29.4c25.801,-25.7 51.4,-51.7 77.4,-77.3l0,0Z" style="fill:#af195a;fill-rule:nonzero;stroke:#7e3794;stroke-width:0.09px;"/>
 7 |   <path d="M106.912,148.98c7.2,-6.9 13.9,-14.3 21.3,-21.1l101.4,101.4c1.7,1.8 3.3,3.6 5.5,4.8c-2.3,1.7 -5.2,2.3 -7.8,3.5c-14.801,6 -29.801,11.6 -44.5,18c-18.301,-0.2 -36.601,-0.1 -54.9,-0.1c-42.6,-0.1 -85.2,0.2 -127.8,-0.1c35.5,-35.6 71.2,-71 106.8,-106.4l0,0Z" style="fill:#ffc112;fill-rule:nonzero;stroke:#ffbb1b;stroke-width:0.09px;"/>
 8 |   <path d="M127.912,255.48c18.3,0 36.6,-0.1 54.9,0.1c17.3,7.1 34.6,13.8 51.899,20.8c-28.399,28.8 -57.099,57.2 -85.599,85.9c-7.2,6.8 -13.7,14.3 -21.3,20.7c0,-42.5 -0.1,-85 0.1,-127.5Z" style="fill:#17a05e;fill-rule:nonzero;stroke:#1a8763;stroke-width:0.09px;"/>
 9 |   <path d="M328.812,255.48l55.1,0c42.5,0.1 85.1,-0.1 127.6,0.1c-27.3,27.7 -55,55.1 -82.399,82.6c-15.2,15.1 -30.2,30.399 -45.4,45.3c-34,-34.4 -68.5,-68.4 -102.6,-102.8c-1.4,-1.5 -2.9,-2.8 -4.601,-3.8c2.9,-1.801 6.101,-2.7 9.2,-4c14.4,-5.8 28.799,-11.4 43.1,-17.4l0,0Z" style="fill:#4587f4;fill-rule:nonzero;stroke:#427fed;stroke-width:0.09px;"/>
10 |   <path d="M234.712,276.38c7.3,17.399 13.9,35 21.2,52.399c-0.1,18.2 0,36.5 -0.1,54.7l0,88c-0.2,13.1 0.3,26.2 -0.2,39.2c-2.101,-1 -3.4,-2.9 -5.101,-4.5c-40.899,-41.099 -81.699,-82.199 -122.699,-123.199c7.6,-6.4 14.1,-13.9 21.3,-20.7c28.5,-28.7 57.2,-57.1 85.6,-85.9Z" style="fill:#8dc44d;fill-rule:nonzero;stroke:#65b045;stroke-width:0.09px;"/>
11 |   <path d="M276.511,276.88c1.7,1 3.2,2.3 4.601,3.8c34.1,34.4 68.6,68.4 102.6,102.8c-42.7,-0.1 -85.3,0.1 -127.899,0c0.1,-18.2 0,-36.5 0.1,-54.7c6.699,-17.3 13.899,-34.5 20.598,-51.9l0,0Z" style="fill:#3569d6;fill-rule:nonzero;stroke:#43459d;stroke-width:0.09px;"/>
12 | </svg>
13 | 


--------------------------------------------------------------------------------
/src/assets/iconWordpress.svg:
--------------------------------------------------------------------------------
 1 | 
 2 | <svg
 3 |   xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128">
 4 |   <g id="_x32__stroke">
 5 |     <g id="Wordpress_1_">
 6 |       <rect clip-rule="evenodd" fill="none" fill-rule="evenodd" height="128" width="128"/>
 7 |       <path clip-rule="evenodd" d="M65.123,69.595l-19.205,55.797    c5.736,1.688,11.8,2.608,18.081,2.608c7.452,0,14.6-1.288,21.253-3.628c-0.168-0.276-0.328-0.564-0.456-0.88L65.123,69.595z     M120.16,33.294c0.276,2.04,0.432,4.224,0.432,6.58c0,6.492-1.216,13.792-4.868,22.924l-19.549,56.517    C115.204,108.223,128,87.606,128,63.998C128,52.87,125.156,42.41,120.16,33.294z M107.204,60.769    c0-7.912-2.844-13.388-5.276-17.648c-3.244-5.276-6.288-9.74-6.288-15.012c0-5.884,4.46-11.36,10.748-11.36    c0.284,0,0.552,0.036,0.828,0.052C95.832,6.368,80.659,0,63.999,0C41.638,0,21.969,11.472,10.525,28.844    c1.504,0.048,2.92,0.076,4.12,0.076c6.692,0,17.057-0.812,17.057-0.812c3.448-0.204,3.856,4.868,0.408,5.272    c0,0-3.468,0.408-7.324,0.612l23.305,69.321l14.008-42.005L52.13,33.992c-3.448-0.204-6.716-0.612-6.716-0.612    c-3.448-0.204-3.044-5.476,0.408-5.272c0,0,10.568,0.812,16.857,0.812c6.692,0,17.057-0.812,17.057-0.812    c3.452-0.204,3.856,4.868,0.408,5.272c0,0-3.472,0.408-7.324,0.612l23.129,68.793l6.388-21.328    C105.096,72.601,107.204,66.245,107.204,60.769z M0,63.997c0,25.332,14.72,47.225,36.069,57.597L5.54,37.952    C1.992,45.909,0,54.717,0,63.997z" fill="#00759D" fill-rule="evenodd" id="Wordpress"/>
 8 |     </g>
 9 |   </g>
10 | </svg>
11 | 


--------------------------------------------------------------------------------
/src/assets/iconZendesk.svg:
--------------------------------------------------------------------------------
 1 | 
 2 | <svg
 3 |   xmlns="http://www.w3.org/2000/svg" viewBox="0 0 152 116">
 4 |   <g>
 5 |     <path d="M70.125,30.375l0,84.675l-70.125,0l70.125,-84.675Z" style="fill:#03363d;fill-rule:nonzero;"/>
 6 |     <path d="M70.125,0c0,19.35 -15.675,35.025 -35.025,35.025c-19.35,0 -35.1,-15.675 -35.1,-35.025l70.125,0Z" style="fill:#03363d;fill-rule:nonzero;"/>
 7 |     <path d="M81.675,115.05c0,-19.35 15.675,-35.025 35.025,-35.025c19.35,0 35.025,15.675 35.025,35.025l-70.05,0Z" style="fill:#03363d;fill-rule:nonzero;"/>
 8 |     <path d="M81.675,84.675l0,-84.675l70.125,0l-70.125,84.675Z" style="fill:#03363d;fill-rule:nonzero;"/>
 9 |   </g>
10 | </svg>
11 | 


--------------------------------------------------------------------------------
/src/components/App.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="app" :class="classes" @keydown.esc="close">
 3 |     <splash-screen v-if="!ready"></splash-screen>
 4 |     <layout v-else></layout>
 5 |     <modal></modal>
 6 |     <notification></notification>
 7 |     <context-menu></context-menu>
 8 |   </div>
 9 | </template>
10 | 
11 | <script>
12 | import '../styles';
13 | import '../styles/markdownHighlighting.scss';
14 | import '../styles/app.scss';
15 | import Layout from './Layout';
16 | import Modal from './Modal';
17 | import Notification from './Notification';
18 | import ContextMenu from './ContextMenu';
19 | import SplashScreen from './SplashScreen';
20 | import syncSvc from '../services/syncSvc';
21 | import networkSvc from '../services/networkSvc';
22 | import tempFileSvc from '../services/tempFileSvc';
23 | import store from '../store';
24 | import './common/vueGlobals';
25 | 
26 | const themeClasses = {
27 |   light: ['app--light'],
28 |   dark: ['app--dark'],
29 | };
30 | 
31 | export default {
32 |   components: {
33 |     Layout,
34 |     Modal,
35 |     Notification,
36 |     ContextMenu,
37 |     SplashScreen,
38 |   },
39 |   data: () => ({
40 |     ready: false,
41 |   }),
42 |   computed: {
43 |     classes() {
44 |       const result = themeClasses[store.getters['data/computedSettings'].colorTheme];
45 |       return Array.isArray(result) ? result : themeClasses.light;
46 |     },
47 |   },
48 |   methods: {
49 |     close() {
50 |       tempFileSvc.close();
51 |     },
52 |   },
53 |   async created() {
54 |     try {
55 |       await syncSvc.init();
56 |       await networkSvc.init();
57 |       this.ready = true;
58 |       tempFileSvc.setReady();
59 |     } catch (err) {
60 |       if (err && err.message === 'RELOAD') {
61 |         window.location.reload();
62 |       } else if (err && err.message !== 'RELOAD') {
63 |         console.error(err); // eslint-disable-line no-console
64 |         store.dispatch('notification/error', err);
65 |       }
66 |     }
67 |   },
68 | };
69 | </script>
70 | 


--------------------------------------------------------------------------------
/src/components/CodeEditor.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <pre class="code-editor textfield prism" :disabled="disabled"></pre>
 3 | </template>
 4 | 
 5 | <script>
 6 | import Prism from 'prismjs';
 7 | import cledit from '../services/editor/cledit';
 8 | 
 9 | export default {
10 |   props: ['value', 'lang', 'disabled'],
11 |   mounted() {
12 |     const preElt = this.$el;
13 |     let scrollElt = preElt;
14 |     while (scrollElt && !scrollElt.classList.contains('modal')) {
15 |       scrollElt = scrollElt.parentNode;
16 |     }
17 |     if (scrollElt) {
18 |       const clEditor = cledit(preElt, scrollElt);
19 |       clEditor.on('contentChanged', value => this.$emit('changed', value));
20 |       clEditor.init({
21 |         content: this.value,
22 |         sectionHighlighter: section => Prism.highlight(section.text, Prism.languages[this.lang]),
23 |       });
24 |       clEditor.toggleEditable(!this.disabled);
25 |     }
26 |   },
27 | };
28 | </script>
29 | 
30 | <style lang="scss">
31 | @import '../styles/variables.scss';
32 | 
33 | .code-editor {
34 |   margin: 0;
35 |   font-family: $font-family-monospace;
36 |   font-size: $font-size-monospace;
37 |   font-variant-ligatures: no-common-ligatures;
38 |   word-break: break-word;
39 |   word-wrap: normal;
40 |   height: auto;
41 |   caret-color: #000;
42 |   min-height: 160px;
43 |   overflow: auto;
44 |   padding: 0.2em 0.4em;
45 | 
46 |   * {
47 |     line-height: $line-height-base;
48 |     font-size: inherit !important;
49 |   }
50 | }
51 | </style>
52 | 


--------------------------------------------------------------------------------
/src/components/ContextMenu.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="context-menu" v-if="items.length" @click="close()" @contextmenu.prevent="close()">
 3 |     <div class="context-menu__inner flex flex--column" :style="{ left: coordinates.left + 'px', top: coordinates.top + 'px' }" @click.stop>
 4 |       <div v-for="(item, idx) in items" :key="idx">
 5 |         <div class="context-menu__separator" v-if="item.type === 'separator'"></div>
 6 |         <div class="context-menu__item context-menu__item--disabled" v-else-if="item.disabled">{{item.name}}</div>
 7 |         <a class="context-menu__item" href="javascript:void(0)" v-else @click="close(item)">{{item.name}}</a>
 8 |       </div>
 9 |     </div>
10 |   </div>
11 | </template>
12 | 
13 | <script>
14 | import { mapState } from 'vuex';
15 | import store from '../store';
16 | 
17 | export default {
18 |   computed: {
19 |     ...mapState('contextMenu', [
20 |       'coordinates',
21 |       'items',
22 |       'resolve',
23 |     ]),
24 |   },
25 |   methods: {
26 |     close(item = null) {
27 |       this.resolve(item);
28 |       store.dispatch('contextMenu/close');
29 |     },
30 |   },
31 | };
32 | </script>
33 | 
34 | <style lang="scss">
35 | .context-menu {
36 |   position: absolute;
37 |   width: 100%;
38 |   height: 100%;
39 |   font-size: 14px;
40 |   line-height: 18px;
41 |   font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
42 |   user-select: none;
43 | }
44 | 
45 | $padding: 5px;
46 | 
47 | .context-menu__inner {
48 |   position: absolute;
49 |   background-color: #ebebeb;
50 |   border-radius: $padding;
51 |   padding: $padding 0;
52 |   box-shadow: 0 6px 10px rgba(0, 0, 0, 0.16), 0 3px 10px 1px rgba(0, 0, 0, 0.12);
53 | }
54 | 
55 | .context-menu__item {
56 |   display: block;
57 |   color: #333;
58 |   text-decoration: none;
59 |   padding: 0 25px;
60 | }
61 | 
62 | a.context-menu__item {
63 |   &:active,
64 |   &:focus,
65 |   &:hover {
66 |     background-color: #338dfc;
67 |     color: #fff;
68 |   }
69 | }
70 | 
71 | .context-menu__item--disabled {
72 |   color: #aaa;
73 | }
74 | 
75 | .context-menu__separator {
76 |   border-top: 2px solid #dcdcdd;
77 |   margin: $padding 0;
78 | }
79 | </style>
80 | 


--------------------------------------------------------------------------------
/src/components/Notification.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="notification">
 3 |     <div class="notification__item flex flex--row flex--align-center" v-for="(item, idx) in items" :key="idx">
 4 |       <div class="notification__icon flex flex--column flex--center">
 5 |         <icon-alert v-if="item.type === 'error'"></icon-alert>
 6 |         <icon-check-circle v-else-if="item.type === 'badge'"></icon-check-circle>
 7 |         <icon-information v-else></icon-information>
 8 |       </div>
 9 |       <div class="notification__content">
10 |         {{item.content}}
11 |       </div>
12 |       <button class="notification__button button" v-if="item.type === 'confirm'" @click="item.reject">
13 |         No
14 |       </button>
15 |       <button class="notification__button button" v-if="item.type === 'confirm'" @click="item.resolve">
16 |         Yes
17 |       </button>
18 |     </div>
19 |   </div>
20 | </template>
21 | 
22 | <script>
23 | import { mapState } from 'vuex';
24 | 
25 | export default {
26 |   computed: mapState('notification', [
27 |     'items',
28 |   ]),
29 | };
30 | </script>
31 | 
32 | <style lang="scss">
33 | @import '../styles/variables.scss';
34 | 
35 | .notification {
36 |   position: absolute;
37 |   bottom: 0;
38 |   right: 0;
39 |   width: 100%;
40 |   max-width: 340px;
41 | }
42 | 
43 | .notification__item {
44 |   margin: 10px;
45 |   padding: 10px 15px;
46 |   line-height: 1.4;
47 |   background-color: #000;
48 |   color: #fff;
49 |   font-size: 0.9em;
50 |   border-radius: $border-radius-base;
51 | }
52 | 
53 | .notification__icon {
54 |   height: 20px;
55 |   width: 20px;
56 |   margin-right: 12px;
57 |   flex: none;
58 | }
59 | 
60 | .notification__button {
61 |   color: $navbar-color;
62 |   padding: 8px;
63 |   flex: none;
64 | 
65 |   &:active,
66 |   &:focus,
67 |   &:hover {
68 |     color: $navbar-hover-color;
69 |     background-color: $navbar-hover-background;
70 |   }
71 | }
72 | </style>
73 | 


--------------------------------------------------------------------------------
/src/components/SplashScreen.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="splash-screen">
 3 |     <div class="splash-screen__inner logo-background"></div>
 4 |   </div>
 5 | </template>
 6 | 
 7 | <style lang="scss">
 8 | .splash-screen {
 9 |   position: absolute;
10 |   top: 0;
11 |   left: 0;
12 |   width: 100%;
13 |   height: 100%;
14 |   padding: 25px;
15 | }
16 | 
17 | .splash-screen__inner {
18 |   margin: 0 auto;
19 |   max-width: 600px;
20 |   height: 100%;
21 | }
22 | </style>
23 | 


--------------------------------------------------------------------------------
/src/components/UserImage.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="user-image" :style="{backgroundImage: url}">
 3 |   </div>
 4 | </template>
 5 | 
 6 | <script>
 7 | import userSvc from '../services/userSvc';
 8 | import store from '../store';
 9 | 
10 | export default {
11 |   props: ['userId'],
12 |   computed: {
13 |     sanitizedUserId() {
14 |       return userSvc.sanitizeUserId(this.userId);
15 |     },
16 |     url() {
17 |       const userInfo = store.state.userInfo.itemsById[this.sanitizedUserId];
18 |       return userInfo && userInfo.imageUrl && `url('${userInfo.imageUrl}')`;
19 |     },
20 |   },
21 |   watch: {
22 |     sanitizedUserId: {
23 |       handler: sanitizedUserId => userSvc.addUserId(sanitizedUserId),
24 |       immediate: true,
25 |     },
26 |   },
27 | };
28 | </script>
29 | 
30 | <style lang="scss">
31 | .user-image {
32 |   width: 100%;
33 |   height: 100%;
34 |   background-color: #fff;
35 |   background-repeat: no-repeat;
36 |   background-position: center;
37 |   background-size: contain;
38 | }
39 | </style>
40 | 


--------------------------------------------------------------------------------
/src/components/UserName.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <span class="user-name">{{name}}</span>
 3 | </template>
 4 | 
 5 | <script>
 6 | import userSvc from '../services/userSvc';
 7 | import store from '../store';
 8 | 
 9 | export default {
10 |   props: ['userId'],
11 |   computed: {
12 |     sanitizedUserId() {
13 |       return userSvc.sanitizeUserId(this.userId);
14 |     },
15 |     name() {
16 |       const userInfo = store.state.userInfo.itemsById[this.sanitizedUserId];
17 |       return userInfo ? userInfo.name : 'Someone';
18 |     },
19 |   },
20 |   watch: {
21 |     sanitizedUserId: {
22 |       handler: sanitizedUserId => userSvc.addUserId(sanitizedUserId),
23 |       immediate: true,
24 |     },
25 |   },
26 | };
27 | </script>
28 | 


--------------------------------------------------------------------------------
/src/components/common/vueGlobals.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue';
 2 | import Clipboard from 'clipboard';
 3 | import timeSvc from '../../services/timeSvc';
 4 | import store from '../../store';
 5 | 
 6 | // Global directives
 7 | Vue.directive('focus', {
 8 |   inserted(el) {
 9 |     el.focus();
10 |     const { value } = el;
11 |     if (value && el.setSelectionRange) {
12 |       el.setSelectionRange(0, value.length);
13 |     }
14 |   },
15 | });
16 | 
17 | const setVisible = (el, value) => {
18 |   el.style.display = value ? '' : 'none';
19 |   if (value) {
20 |     el.removeAttribute('aria-hidden');
21 |   } else {
22 |     el.setAttribute('aria-hidden', 'true');
23 |   }
24 | };
25 | Vue.directive('show', {
26 |   bind(el, { value }) {
27 |     setVisible(el, value);
28 |   },
29 |   update(el, { value, oldValue }) {
30 |     if (value !== oldValue) {
31 |       setVisible(el, value);
32 |     }
33 |   },
34 | });
35 | 
36 | const setElTitle = (el, title) => {
37 |   el.title = title;
38 |   el.setAttribute('aria-label', title);
39 | };
40 | Vue.directive('title', {
41 |   bind(el, { value }) {
42 |     setElTitle(el, value);
43 |   },
44 |   update(el, { value, oldValue }) {
45 |     if (value !== oldValue) {
46 |       setElTitle(el, value);
47 |     }
48 |   },
49 | });
50 | 
51 | // Clipboard directive
52 | const createClipboard = (el, value) => {
53 |   el.seClipboard = new Clipboard(el, { text: () => value });
54 | };
55 | const destroyClipboard = (el) => {
56 |   if (el.seClipboard) {
57 |     el.seClipboard.destroy();
58 |     el.seClipboard = null;
59 |   }
60 | };
61 | Vue.directive('clipboard', {
62 |   bind(el, { value }) {
63 |     createClipboard(el, value);
64 |   },
65 |   update(el, { value, oldValue }) {
66 |     if (value !== oldValue) {
67 |       destroyClipboard(el);
68 |       createClipboard(el, value);
69 |     }
70 |   },
71 |   unbind(el) {
72 |     destroyClipboard(el);
73 |   },
74 | });
75 | 
76 | // Global filters
77 | Vue.filter('formatTime', time =>
78 |   // Access the time counter for reactive refresh
79 |   timeSvc.format(time, store.state.timeCounter));
80 | 
81 | 


--------------------------------------------------------------------------------
/src/components/gutters/EditorNewDiscussionButton.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <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)">
 3 |     <icon-message></icon-message>
 4 |   </a>
 5 | </template>
 6 | 
 7 | <script>
 8 | import { mapActions } from 'vuex';
 9 | import editorSvc from '../../services/editorSvc';
10 | import store from '../../store';
11 | 
12 | export default {
13 |   data: () => ({
14 |     selection: null,
15 |     coordinates: null,
16 |   }),
17 |   methods: {
18 |     ...mapActions('discussion', [
19 |       'createNewDiscussion',
20 |     ]),
21 |     checkSelection() {
22 |       clearTimeout(this.timeout);
23 |       this.timeout = setTimeout(() => {
24 |         let offset;
25 |         // Show the button if content is not a revision and has the focus
26 |         if (
27 |           !store.state.content.revisionContent &&
28 |           editorSvc.clEditor.selectionMgr.hasFocus()
29 |         ) {
30 |           this.selection = editorSvc.getTrimmedSelection();
31 |           if (this.selection) {
32 |             const text = editorSvc.clEditor.getContent();
33 |             offset = this.selection.end;
34 |             while (offset && text[offset - 1] === '\n') {
35 |               offset -= 1;
36 |             }
37 |           }
38 |         }
39 |         this.coordinates = offset
40 |           ? editorSvc.clEditor.selectionMgr.getCoordinates(offset)
41 |           : null;
42 |       }, 25);
43 |     },
44 |   },
45 |   mounted() {
46 |     this.$nextTick(() => {
47 |       editorSvc.clEditor.selectionMgr.on('selectionChanged', () => this.checkSelection());
48 |       editorSvc.clEditor.selectionMgr.on('cursorCoordinatesChanged', () => this.checkSelection());
49 |       editorSvc.clEditor.on('focus', () => this.checkSelection());
50 |       editorSvc.clEditor.on('blur', () => this.checkSelection());
51 |       this.checkSelection();
52 |     });
53 |   },
54 | };
55 | </script>
56 | 


--------------------------------------------------------------------------------
/src/components/gutters/PreviewNewDiscussionButton.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <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)">
 3 |     <icon-message></icon-message>
 4 |   </a>
 5 | </template>
 6 | 
 7 | <script>
 8 | import { mapActions } from 'vuex';
 9 | import editorSvc from '../../services/editorSvc';
10 | import store from '../../store';
11 | 
12 | export default {
13 |   data: () => ({
14 |     selection: null,
15 |     coordinates: null,
16 |   }),
17 |   methods: {
18 |     ...mapActions('discussion', [
19 |       'createNewDiscussion',
20 |     ]),
21 |     checkSelection() {
22 |       clearTimeout(this.timeout);
23 |       this.timeout = setTimeout(() => {
24 |         let offset;
25 |         // Show the button if content is not a revision and preview selection is not empty
26 |         if (
27 |           !store.state.content.revisionContent &&
28 |           editorSvc.previewSelectionRange
29 |         ) {
30 |           this.selection = editorSvc.getTrimmedSelection();
31 |           if (this.selection) {
32 |             const { text } = editorSvc.previewCtxWithDiffs;
33 |             offset = editorSvc.getPreviewOffset(this.selection.end);
34 |             while (offset && text[offset - 1] === '\n') {
35 |               offset -= 1;
36 |             }
37 |           }
38 |         }
39 |         this.coordinates = offset
40 |           ? editorSvc.getPreviewOffsetCoordinates(offset)
41 |           : null;
42 |       }, 25);
43 |     },
44 |   },
45 |   mounted() {
46 |     this.$nextTick(() => {
47 |       editorSvc.$on('previewSelectionRange', () => this.checkSelection());
48 |       this.$watch(
49 |         () => store.getters['layout/styles'].previewWidth,
50 |         () => this.checkSelection(),
51 |       );
52 |       this.checkSelection();
53 |     });
54 |   },
55 | };
56 | </script>
57 | 


--------------------------------------------------------------------------------
/src/components/gutters/StickyComment.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="sticky-comment" :style="{width: constants.gutterWidth + 'px', top: top + 'px'}">
 3 |     <comment v-if="currentDiscussionLastComment" :comment="currentDiscussionLastComment"></comment>
 4 |     <new-comment v-if="isCommenting"></new-comment>
 5 |   </div>
 6 | </template>
 7 | 
 8 | <script>
 9 | import { mapState, mapGetters } from 'vuex';
10 | import Comment from './Comment';
11 | import NewComment from './NewComment';
12 | 
13 | export default {
14 |   components: {
15 |     Comment,
16 |     NewComment,
17 |   },
18 |   data: () => ({
19 |     top: 0,
20 |   }),
21 |   computed: {
22 |     ...mapGetters('layout', [
23 |       'constants',
24 |     ]),
25 |     ...mapState('discussion', [
26 |       'isCommenting',
27 |     ]),
28 |     ...mapGetters('discussion', [
29 |       'currentDiscussionLastComment',
30 |     ]),
31 |   },
32 | };
33 | </script>
34 | 
35 | <style lang="scss">
36 | @import '../../styles/variables.scss';
37 | 
38 | .sticky-comment {
39 |   position: absolute;
40 |   right: 0;
41 |   font-size: 15px;
42 |   padding-top: 10px;
43 | 
44 |   .current-discussion & {
45 |     width: auto !important;
46 |   }
47 | }
48 | </style>
49 | 


--------------------------------------------------------------------------------
/src/components/menus/WorkspaceBackupMenu.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="side-bar__panel side-bar__panel--menu">
 3 |     <input class="hidden-file" id="import-backup-file-input" type="file" @change="onImportBackup">
 4 |     <label class="menu-entry button flex flex--row flex--align-center" for="import-backup-file-input">
 5 |       <div class="menu-entry__icon flex flex--column flex--center">
 6 |         <icon-content-save></icon-content-save>
 7 |       </div>
 8 |       <div class="flex flex--column">
 9 |         Import workspace backup
10 |       </div>
11 |     </label>
12 |     <menu-entry @click.native="exportWorkspace">
13 |       <icon-content-save slot="icon"></icon-content-save>
14 |       Export workspace backup
15 |     </menu-entry>
16 |   </div>
17 | </template>
18 | 
19 | <script>
20 | import FileSaver from 'file-saver';
21 | import MenuEntry from './common/MenuEntry';
22 | import store from '../../store';
23 | import backupSvc from '../../services/backupSvc';
24 | import localDbSvc from '../../services/localDbSvc';
25 | 
26 | export default {
27 |   components: {
28 |     MenuEntry,
29 |   },
30 |   computed: {
31 |     workspaceId: () => store.getters['workspace/currentWorkspace'].id,
32 |   },
33 |   methods: {
34 |     onImportBackup(evt) {
35 |       const file = evt.target.files[0];
36 |       if (file) {
37 |         const reader = new FileReader();
38 |         reader.onload = (e) => {
39 |           const text = e.target.result;
40 |           if (text.match(/\uFFFD/)) {
41 |             store.dispatch('notification/error', 'File is not readable.');
42 |           } else {
43 |             backupSvc.importBackup(text);
44 |           }
45 |         };
46 |         const blob = file.slice(0, 10000000);
47 |         reader.readAsText(blob);
48 |       }
49 |     },
50 |     exportWorkspace() {
51 |       const allItemsById = {};
52 |       localDbSvc.getWorkspaceItems(this.workspaceId, (item) => {
53 |         allItemsById[item.id] = item;
54 |       }, () => {
55 |         const backup = JSON.stringify(allItemsById);
56 |         const blob = new Blob([backup], {
57 |           type: 'text/plain;charset=utf-8',
58 |         });
59 |         FileSaver.saveAs(blob, 'StackEdit workspace.json');
60 |       });
61 |     },
62 |   },
63 | };
64 | </script>
65 | 


--------------------------------------------------------------------------------
/src/components/menus/common/MenuEntry.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <a class="menu-entry button flex flex--row flex--align-center" href="javascript:void(0)">
 3 |     <div class="menu-entry__icon flex flex--column flex--center">
 4 |       <slot name="icon"></slot>
 5 |     </div>
 6 |     <div class="menu-entry__text flex flex--column">
 7 |       <slot></slot>
 8 |     </div>
 9 |   </a>
10 | </template>
11 | 
12 | <style lang="scss">
13 | @import '../../../styles/variables.scss';
14 | 
15 | .menu-entry {
16 |   text-align: left;
17 |   padding: 10px;
18 |   height: auto;
19 |   font-size: 17px;
20 |   line-height: 1.4;
21 |   text-transform: none;
22 |   white-space: normal;
23 | 
24 |   span {
25 |     display: inline-block;
26 |     font-size: 0.75rem;
27 |     opacity: 0.67;
28 |     line-height: 1.3;
29 | 
30 |     .menu-entry__label {
31 |       opacity: 1;
32 |     }
33 | 
34 |     span {
35 |       display: inline;
36 |       opacity: 1;
37 |     }
38 |   }
39 | }
40 | 
41 | .menu-entry--info {
42 |   padding-top: 3px;
43 |   padding-bottom: 3px;
44 | }
45 | 
46 | .menu-entry__icon {
47 |   height: 20px;
48 |   width: 20px;
49 |   margin-right: 12px;
50 |   flex: none;
51 | }
52 | 
53 | .menu-entry__icon--disabled {
54 |   opacity: 0.5;
55 | }
56 | 
57 | .menu-entry__icon--image {
58 |   border-radius: $border-radius-base;
59 |   overflow: hidden;
60 | }
61 | 
62 | .hidden-file {
63 |   position: fixed;
64 |   top: -999px;
65 | }
66 | 
67 | .menu-entry__label {
68 |   float: right;
69 |   font-size: 0.6rem;
70 |   font-weight: 600;
71 |   line-height: 1;
72 |   padding: 0.15em 0.25em;
73 |   background-color: #fff;
74 |   border-radius: 3px;
75 |   opacity: 0.6;
76 | }
77 | 
78 | .menu-entry__label--warning {
79 |   color: #fff;
80 |   background-color: darken($error-color, 10);
81 |   opacity: 1;
82 | }
83 | 
84 | .menu-entry__label--count {
85 |   font-size: 0.75rem;
86 |   font-weight: 400;
87 | }
88 | 
89 | .menu-entry__text {
90 |   width: 100%;
91 |   overflow: hidden;
92 | }
93 | </style>
94 | 


--------------------------------------------------------------------------------
/src/components/modals/HtmlExportModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Export to HTML">
 3 |     <div class="modal__content">
 4 |       <p>Please choose a template for your <b>HTML export</b>.</p>
 5 |       <form-entry label="Template">
 6 |         <select class="textfield" slot="field" v-model="selectedTemplate" @keydown.enter="resolve()">
 7 |           <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
 8 |             {{ template.name }}
 9 |           </option>
10 |         </select>
11 |         <div class="form-entry__actions">
12 |           <a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
13 |         </div>
14 |       </form-entry>
15 |     </div>
16 |     <div class="modal__button-bar">
17 |       <button class="button button--copy" v-clipboard="result" @click="info('HTML copied to clipboard!')">Copy</button>
18 |       <button class="button" @click="config.reject()">Cancel</button>
19 |       <button class="button button--resolve" @click="resolve()">Ok</button>
20 |     </div>
21 |   </modal-inner>
22 | </template>
23 | 
24 | <script>
25 | import { mapActions } from 'vuex';
26 | import exportSvc from '../../services/exportSvc';
27 | import modalTemplate from './common/modalTemplate';
28 | import store from '../../store';
29 | import badgeSvc from '../../services/badgeSvc';
30 | 
31 | export default modalTemplate({
32 |   data: () => ({
33 |     result: '',
34 |   }),
35 |   computedLocalSettings: {
36 |     selectedTemplate: 'htmlExportTemplate',
37 |   },
38 |   mounted() {
39 |     let timeoutId;
40 |     this.$watch('selectedTemplate', (selectedTemplate) => {
41 |       clearTimeout(timeoutId);
42 |       timeoutId = setTimeout(async () => {
43 |         const currentFile = store.getters['file/current'];
44 |         const html = await exportSvc.applyTemplate(
45 |           currentFile.id,
46 |           this.allTemplatesById[selectedTemplate],
47 |         );
48 |         this.result = html;
49 |       }, 10);
50 |     }, {
51 |       immediate: true,
52 |     });
53 |   },
54 |   methods: {
55 |     ...mapActions('notification', [
56 |       'info',
57 |     ]),
58 |     async resolve() {
59 |       const { config } = this;
60 |       const currentFile = store.getters['file/current'];
61 |       config.resolve();
62 |       await exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplatesById[this.selectedTemplate]);
63 |       badgeSvc.addBadge('exportHtml');
64 |     },
65 |   },
66 | });
67 | </script>
68 | 


--------------------------------------------------------------------------------
/src/components/modals/LinkModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Insert link">
 3 |     <div class="modal__content">
 4 |       <p>Please provide a <b>URL</b> for your link.</p>
 5 |       <form-entry label="URL" error="url">
 6 |         <input slot="field" class="textfield" type="text" v-model.trim="url" @keydown.enter="resolve">
 7 |       </form-entry>
 8 |     </div>
 9 |     <div class="modal__button-bar">
10 |       <button class="button" @click="reject()">Cancel</button>
11 |       <button class="button button--resolve" @click="resolve">Ok</button>
12 |     </div>
13 |   </modal-inner>
14 | </template>
15 | 
16 | <script>
17 | import modalTemplate from './common/modalTemplate';
18 | 
19 | export default modalTemplate({
20 |   data: () => ({
21 |     url: '',
22 |   }),
23 |   methods: {
24 |     resolve(evt) {
25 |       evt.preventDefault(); // Fixes https://github.com/benweet/stackedit/issues/1503
26 |       if (!this.url) {
27 |         this.setError('url');
28 |       } else {
29 |         const { callback } = this.config;
30 |         this.config.resolve();
31 |         callback(this.url);
32 |       }
33 |     },
34 |     reject() {
35 |       const { callback } = this.config;
36 |       this.config.reject();
37 |       callback(null);
38 |     },
39 |   },
40 | });
41 | </script>
42 | 


--------------------------------------------------------------------------------
/src/components/modals/common/FormEntry.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="form-entry" :error="error">
 3 |     <label class="form-entry__label" :for="uid">{{label}}<span class="form-entry__label-info" v-if="info"> &mdash; {{info}}</span></label>
 4 |     <div class="form-entry__field">
 5 |       <slot name="field"></slot>
 6 |     </div>
 7 |     <slot></slot>
 8 |   </div>
 9 | </template>
10 | 
11 | <script>
12 | import utils from '../../../services/utils';
13 | 
14 | export default {
15 |   props: ['label', 'info', 'error'],
16 |   data: () => ({
17 |     uid: utils.uid(),
18 |   }),
19 |   mounted() {
20 |     this.$el.querySelector('input,select').id = this.uid;
21 |   },
22 | };
23 | </script>
24 | 


--------------------------------------------------------------------------------
/src/components/modals/common/ModalInner.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="modal__inner-1" role="dialog">
 3 |     <div class="modal__inner-2">
 4 |       <button class="modal__close-button button not-tabbable" @click="config.reject()" v-title="'Close modal'">
 5 |         <icon-close></icon-close>
 6 |       </button>
 7 |       <slot></slot>
 8 |     </div>
 9 |   </div>
10 | </template>
11 | 
12 | <script>
13 | import { mapGetters } from 'vuex';
14 | 
15 | export default {
16 |   computed: {
17 |     ...mapGetters('modal', [
18 |       'config',
19 |     ]),
20 |   },
21 | };
22 | </script>
23 | 
24 | <style lang="scss">
25 | @import '../../../styles/variables.scss';
26 | 
27 | .modal__close-button {
28 |   position: absolute;
29 |   top: 8px;
30 |   right: 8px;
31 |   color: rgba(0, 0, 0, 0.5);
32 |   width: 32px;
33 |   height: 32px;
34 |   padding: 2px;
35 | 
36 |   &:active,
37 |   &:focus,
38 |   &:hover {
39 |     color: rgba(0, 0, 0, 0.67);
40 |   }
41 | }
42 | </style>
43 | 


--------------------------------------------------------------------------------
/src/components/modals/common/Tab.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="tabs__tab flex flex--row" :class="{'tabs__tab--active': active}" role="tab">
 3 |     <a class="flex flex--column flex--center" href="javascript:void(0)" @click="$emit('click')">
 4 |       <slot></slot>
 5 |     </a>
 6 |   </div>
 7 | </template>
 8 | 
 9 | <script>
10 | export default {
11 |   props: ['active'],
12 | };
13 | </script>
14 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/CouchdbCredentialsModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Insert image">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="couchdb"></icon-provider>
 6 |       </div>
 7 |       <p>Please provide your credentials to login to <b>CouchDB</b>.</p>
 8 |       <form-entry label="Name" error="name">
 9 |         <input slot="field" class="textfield" type="text" v-model.trim="name" @keydown.enter="resolve()">
10 |       </form-entry>
11 |       <form-entry label="Password" error="password">
12 |         <input slot="field" class="textfield" type="password" v-model.trim="password" @keydown.enter="resolve()">
13 |       </form-entry>
14 |     </div>
15 |     <div class="modal__button-bar">
16 |       <button class="button" @click="config.reject()">Cancel</button>
17 |       <button class="button button--resolve" @click="resolve()">Ok</button>
18 |     </div>
19 |   </modal-inner>
20 | </template>
21 | 
22 | <script>
23 | import modalTemplate from '../common/modalTemplate';
24 | import store from '../../../store';
25 | 
26 | export default modalTemplate({
27 |   data: () => ({
28 |     name: '',
29 |     password: '',
30 |   }),
31 |   created() {
32 |     this.name = this.config.token.name;
33 |     this.password = this.config.token.password;
34 |   },
35 |   methods: {
36 |     resolve() {
37 |       if (!this.name) {
38 |         this.setError('name');
39 |       }
40 |       if (!this.password) {
41 |         this.setError('password');
42 |       }
43 |       if (this.name && this.password) {
44 |         const token = {
45 |           ...this.config.token,
46 |           name: this.name,
47 |           password: this.password,
48 |         };
49 |         store.dispatch('data/addCouchdbToken', token);
50 |         this.config.resolve();
51 |       }
52 |     },
53 |   },
54 | });
55 | </script>
56 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/CouchdbWorkspaceModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Add CouchDB workspace">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="couchdb"></icon-provider>
 6 |       </div>
 7 |       <p>Create a workspace synced with a <b>CouchDB</b> database.</p>
 8 |       <form-entry label="Database URL" error="dbUrl">
 9 |         <input slot="field" class="textfield" type="text" v-model.trim="dbUrl" @keydown.enter="resolve()">
10 |         <div class="form-entry__info">
11 |           <b>Example:</b> https://instance.smileupps.com/stackedit-workspace
12 |         </div>
13 |         <div class="form-entry__actions">
14 |           <a href="https://community.stackedit.io/t/couchdb-workspace-setup/" target="_blank">How to setup?</a>
15 |         </div>
16 |       </form-entry>
17 |     </div>
18 |     <div class="modal__button-bar">
19 |       <button class="button" @click="config.reject()">Cancel</button>
20 |       <button class="button button--resolve" @click="resolve()">Ok</button>
21 |     </div>
22 |   </modal-inner>
23 | </template>
24 | 
25 | <script>
26 | import modalTemplate from '../common/modalTemplate';
27 | import utils from '../../../services/utils';
28 | 
29 | export default modalTemplate({
30 |   data: () => ({
31 |     dbUrl: '',
32 |   }),
33 |   methods: {
34 |     resolve() {
35 |       if (!this.dbUrl) {
36 |         this.setError('dbUrl');
37 |       } else {
38 |         const url = utils.addQueryParams('app', {
39 |           providerId: 'couchdbWorkspace',
40 |           dbUrl: this.dbUrl,
41 |         }, true);
42 |         this.config.resolve();
43 |         window.open(url);
44 |       }
45 |     },
46 |   },
47 | });
48 | </script>
49 | 
50 | <style lang="scss">
51 | .couchdb-workspace__info {
52 |   font-size: 0.8em;
53 | }
54 | </style>
55 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/DropboxAccountModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Link Dropbox account">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="dropbox"></icon-provider>
 6 |       </div>
 7 |       <p>Link your <b>Dropbox</b> account to <b>StackEdit</b>.</p>
 8 |       <div class="form-entry">
 9 |         <div class="form-entry__checkbox">
10 |           <label>
11 |             <input type="checkbox" v-model="restrictedAccess"> Restrict access
12 |           </label>
13 |           <div class="form-entry__info">
14 |             If checked, access will be restricted to the <b>/Applications/StackEdit (restricted)</b> folder.
15 |           </div>
16 |         </div>
17 |       </div>
18 |     </div>
19 |     <div class="modal__button-bar">
20 |       <button class="button" @click="config.reject()">Cancel</button>
21 |       <button class="button button--resolve" @click="config.resolve()">Ok</button>
22 |     </div>
23 |   </modal-inner>
24 | </template>
25 | 
26 | <script>
27 | import modalTemplate from '../common/modalTemplate';
28 | 
29 | export default modalTemplate({
30 |   computedLocalSettings: {
31 |     restrictedAccess: 'dropboxRestrictedAccess',
32 |   },
33 | });
34 | </script>
35 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/DropboxPublishModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Publish to Dropbox">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="dropbox"></icon-provider>
 6 |       </div>
 7 |       <p>Publish <b>{{currentFileName}}</b> to your <b>Dropbox</b>.</p>
 8 |       <form-entry label="File path" error="path">
 9 |         <input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
10 |         <div class="form-entry__info">
11 |           <b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.html<br>
12 |           If the file exists, it will be overwritten.
13 |         </div>
14 |       </form-entry>
15 |       <form-entry label="Template">
16 |         <select slot="field" class="textfield" v-model="selectedTemplate" @keydown.enter="resolve()">
17 |           <option v-for="(template, id) in allTemplatesById" :key="id" :value="id">
18 |             {{ template.name }}
19 |           </option>
20 |         </select>
21 |         <div class="form-entry__actions">
22 |           <a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
23 |         </div>
24 |       </form-entry>
25 |     </div>
26 |     <div class="modal__button-bar">
27 |       <button class="button" @click="config.reject()">Cancel</button>
28 |       <button class="button button--resolve" @click="resolve()">Ok</button>
29 |     </div>
30 |   </modal-inner>
31 | </template>
32 | 
33 | <script>
34 | import dropboxProvider from '../../../services/providers/dropboxProvider';
35 | import modalTemplate from '../common/modalTemplate';
36 | 
37 | export default modalTemplate({
38 |   data: () => ({
39 |     path: '',
40 |   }),
41 |   computedLocalSettings: {
42 |     selectedTemplate: 'dropboxPublishTemplate',
43 |   },
44 |   created() {
45 |     this.path = `/${this.currentFileName}.html`;
46 |   },
47 |   methods: {
48 |     resolve() {
49 |       if (!dropboxProvider.checkPath(this.path)) {
50 |         this.setError('path');
51 |       } else {
52 |         // Return new location
53 |         const location = dropboxProvider.makeLocation(this.config.token, this.path);
54 |         location.templateId = this.selectedTemplate;
55 |         this.config.resolve(location);
56 |       }
57 |     },
58 |   },
59 | });
60 | </script>
61 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/DropboxSaveModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Synchronize with Dropbox">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="dropbox"></icon-provider>
 6 |       </div>
 7 |       <p>Save <b>{{currentFileName}}</b> to your <b>Dropbox</b> and keep it synced.</p>
 8 |       <form-entry label="File path" error="path">
 9 |         <input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
10 |         <div class="form-entry__info">
11 |           <b>Example:</b> {{config.token.fullAccess ? '' : '/Applications/StackEdit (restricted)'}}/path/to/My Document.md<br>
12 |           If the file exists, it will be overwritten.
13 |         </div>
14 |       </form-entry>
15 |     </div>
16 |     <div class="modal__button-bar">
17 |       <button class="button" @click="config.reject()">Cancel</button>
18 |       <button class="button button--resolve" @click="resolve()">Ok</button>
19 |     </div>
20 |   </modal-inner>
21 | </template>
22 | 
23 | <script>
24 | import dropboxProvider from '../../../services/providers/dropboxProvider';
25 | import modalTemplate from '../common/modalTemplate';
26 | 
27 | export default modalTemplate({
28 |   data: () => ({
29 |     path: '',
30 |   }),
31 |   created() {
32 |     this.path = `/${this.currentFileName}.md`;
33 |   },
34 |   methods: {
35 |     resolve() {
36 |       if (!dropboxProvider.checkPath(this.path)) {
37 |         this.setError('path');
38 |       } else {
39 |         // Return new location
40 |         const location = dropboxProvider.makeLocation(this.config.token, this.path);
41 |         this.config.resolve(location);
42 |       }
43 |     },
44 |   },
45 | });
46 | </script>
47 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/GistSyncModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Synchronize with Gist">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="gist"></icon-provider>
 6 |       </div>
 7 |       <p>Save <b>{{currentFileName}}</b> to a <b>Gist</b> and keep it synced.</p>
 8 |       <form-entry label="Filename" error="filename">
 9 |         <input slot="field" class="textfield" type="text" v-model.trim="filename" @keydown.enter="resolve()">
10 |       </form-entry>
11 |       <div class="form-entry">
12 |         <div class="form-entry__checkbox">
13 |           <label>
14 |             <input type="checkbox" v-model="isPublic"> Public
15 |           </label>
16 |         </div>
17 |       </div>
18 |       <form-entry label="Existing Gist ID" info="optional">
19 |         <input slot="field" class="textfield" type="text" v-model.trim="gistId" @keydown.enter="resolve()">
20 |         <div class="form-entry__info">
21 |           If the file exists in the Gist, it will be overwritten.
22 |         </div>
23 |       </form-entry>
24 |     </div>
25 |     <div class="modal__button-bar">
26 |       <button class="button" @click="config.reject()">Cancel</button>
27 |       <button class="button button--resolve" @click="resolve()">Ok</button>
28 |     </div>
29 |   </modal-inner>
30 | </template>
31 | 
32 | <script>
33 | import gistProvider from '../../../services/providers/gistProvider';
34 | import modalTemplate from '../common/modalTemplate';
35 | 
36 | export default modalTemplate({
37 |   data: () => ({
38 |     filename: '',
39 |     gistId: '',
40 |   }),
41 |   computedLocalSettings: {
42 |     isPublic: 'gistIsPublic',
43 |   },
44 |   created() {
45 |     this.filename = `${this.currentFileName}.md`;
46 |   },
47 |   methods: {
48 |     resolve() {
49 |       if (!this.filename) {
50 |         this.setError('filename');
51 |       } else {
52 |         // Return new location
53 |         const location = gistProvider.makeLocation(
54 |           this.config.token,
55 |           this.filename,
56 |           this.isPublic,
57 |           this.gistId,
58 |         );
59 |         this.config.resolve(location);
60 |       }
61 |     },
62 |   },
63 | });
64 | </script>
65 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/GithubAccountModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Link GitHub account">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="github"></icon-provider>
 6 |       </div>
 7 |       <p>Link your <b>GitHub</b> account to <b>StackEdit</b>.</p>
 8 |       <div class="form-entry">
 9 |         <div class="form-entry__checkbox">
10 |           <label>
11 |             <input type="checkbox" v-model="repoFullAccess"> Grant access to your private repositories
12 |           </label>
13 |         </div>
14 |       </div>
15 |     </div>
16 |     <div class="modal__button-bar">
17 |       <button class="button" @click="config.reject()">Cancel</button>
18 |       <button class="button button--resolve" @click="config.resolve()">Ok</button>
19 |     </div>
20 |   </modal-inner>
21 | </template>
22 | 
23 | <script>
24 | import modalTemplate from '../common/modalTemplate';
25 | 
26 | export default modalTemplate({
27 |   computedLocalSettings: {
28 |     repoFullAccess: 'githubRepoFullAccess',
29 |   },
30 | });
31 | </script>
32 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/GithubOpenModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Synchronize with GitHub">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="github"></icon-provider>
 6 |       </div>
 7 |       <p>Open a file from your <b>GitHub</b> repository and keep it synced.</p>
 8 |       <form-entry label="Repository URL" error="repoUrl">
 9 |         <input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
10 |         <div class="form-entry__info">
11 |           <b>Example:</b> https://github.com/owner/my-repo
12 |         </div>
13 |       </form-entry>
14 |       <form-entry label="File path" error="path">
15 |         <input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
16 |         <div class="form-entry__info">
17 |           <b>Example:</b> path/to/README.md
18 |         </div>
19 |       </form-entry>
20 |       <form-entry label="Branch" info="optional">
21 |         <input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
22 |         <div class="form-entry__info">
23 |           If not supplied, the <code>master</code> branch will be used.
24 |         </div>
25 |       </form-entry>
26 |     </div>
27 |     <div class="modal__button-bar">
28 |       <button class="button" @click="config.reject()">Cancel</button>
29 |       <button class="button button--resolve" @click="resolve()">Ok</button>
30 |     </div>
31 |   </modal-inner>
32 | </template>
33 | 
34 | <script>
35 | import githubProvider from '../../../services/providers/githubProvider';
36 | import modalTemplate from '../common/modalTemplate';
37 | import utils from '../../../services/utils';
38 | 
39 | export default modalTemplate({
40 |   data: () => ({
41 |     branch: '',
42 |     path: '',
43 |   }),
44 |   computedLocalSettings: {
45 |     repoUrl: 'githubRepoUrl',
46 |   },
47 |   methods: {
48 |     resolve() {
49 |       const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
50 |       if (!parsedRepo) {
51 |         this.setError('repoUrl');
52 |       }
53 |       if (!this.path) {
54 |         this.setError('path');
55 |       }
56 |       if (parsedRepo && this.path) {
57 |         // Return new location
58 |         const location = githubProvider.makeLocation(
59 |           this.config.token,
60 |           parsedRepo.owner,
61 |           parsedRepo.repo,
62 |           this.branch || 'master',
63 |           this.path,
64 |         );
65 |         this.config.resolve(location);
66 |       }
67 |     },
68 |   },
69 | });
70 | </script>
71 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/GithubWorkspaceModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Synchronize with GitHub">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="github"></icon-provider>
 6 |       </div>
 7 |       <p>Create a workspace synced with a <b>GitHub</b> repository folder.</p>
 8 |       <form-entry label="Repository URL" error="repoUrl">
 9 |         <input slot="field" class="textfield" type="text" v-model.trim="repoUrl" @keydown.enter="resolve()">
10 |         <div class="form-entry__info">
11 |           <b>Example:</b> https://github.com/owner/my-repo
12 |         </div>
13 |       </form-entry>
14 |       <form-entry label="Folder path" info="optional">
15 |         <input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
16 |         <div class="form-entry__info">
17 |           If not supplied, the root folder will be used.
18 |         </div>
19 |       </form-entry>
20 |       <form-entry label="Branch" info="optional">
21 |         <input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
22 |         <div class="form-entry__info">
23 |           If not supplied, the <code>master</code> branch will be used.
24 |         </div>
25 |       </form-entry>
26 |     </div>
27 |     <div class="modal__button-bar">
28 |       <button class="button" @click="config.reject()">Cancel</button>
29 |       <button class="button button--resolve" @click="resolve()">Ok</button>
30 |     </div>
31 |   </modal-inner>
32 | </template>
33 | 
34 | <script>
35 | import utils from '../../../services/utils';
36 | import modalTemplate from '../common/modalTemplate';
37 | 
38 | export default modalTemplate({
39 |   data: () => ({
40 |     branch: '',
41 |     path: '',
42 |   }),
43 |   computedLocalSettings: {
44 |     repoUrl: 'githubWorkspaceRepoUrl',
45 |   },
46 |   methods: {
47 |     resolve() {
48 |       const parsedRepo = utils.parseGithubRepoUrl(this.repoUrl);
49 |       if (!parsedRepo) {
50 |         this.setError('repoUrl');
51 |       } else {
52 |         const path = this.path && this.path.replace(/^\//, '');
53 |         const url = utils.addQueryParams('app', {
54 |           ...parsedRepo,
55 |           providerId: 'githubWorkspace',
56 |           branch: this.branch || 'master',
57 |           path: path || undefined,
58 |         }, true);
59 |         this.config.resolve();
60 |         window.open(url);
61 |       }
62 |     },
63 |   },
64 | });
65 | </script>
66 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/GitlabOpenModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Synchronize with GitLab">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="gitlab"></icon-provider>
 6 |       </div>
 7 |       <p>Open a file from your <b>GitLab</b> project and keep it synced.</p>
 8 |       <form-entry label="Project URL" error="projectUrl">
 9 |         <input slot="field" class="textfield" type="text" v-model.trim="projectUrl" @keydown.enter="resolve()">
10 |         <div class="form-entry__info">
11 |           <b>Example:</b> {{config.token.serverUrl}}/path/to/project
12 |         </div>
13 |       </form-entry>
14 |       <form-entry label="File path" error="path">
15 |         <input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
16 |         <div class="form-entry__info">
17 |           <b>Example:</b> path/to/README.md
18 |         </div>
19 |       </form-entry>
20 |       <form-entry label="Branch" info="optional">
21 |         <input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
22 |         <div class="form-entry__info">
23 |           If not supplied, the <code>master</code> branch will be used.
24 |         </div>
25 |       </form-entry>
26 |     </div>
27 |     <div class="modal__button-bar">
28 |       <button class="button" @click="config.reject()">Cancel</button>
29 |       <button class="button button--resolve" @click="resolve()">Ok</button>
30 |     </div>
31 |   </modal-inner>
32 | </template>
33 | 
34 | <script>
35 | import gitlabProvider from '../../../services/providers/gitlabProvider';
36 | import modalTemplate from '../common/modalTemplate';
37 | import utils from '../../../services/utils';
38 | 
39 | export default modalTemplate({
40 |   data: () => ({
41 |     branch: '',
42 |     path: '',
43 |   }),
44 |   computedLocalSettings: {
45 |     projectUrl: 'gitlabProjectUrl',
46 |   },
47 |   methods: {
48 |     resolve() {
49 |       const projectPath = utils.parseGitlabProjectPath(this.projectUrl);
50 |       if (!projectPath) {
51 |         this.setError('projectUrl');
52 |       }
53 |       if (!this.path) {
54 |         this.setError('path');
55 |       }
56 |       if (projectPath && this.path) {
57 |         // Return new location
58 |         const location = gitlabProvider.makeLocation(
59 |           this.config.token,
60 |           projectPath,
61 |           this.branch || 'master',
62 |           this.path,
63 |         );
64 |         this.config.resolve(location);
65 |       }
66 |     },
67 |   },
68 | });
69 | </script>
70 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/GitlabWorkspaceModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Synchronize with GitLab">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="gitlab"></icon-provider>
 6 |       </div>
 7 |       <p>Create a workspace synced with a <b>GitLab</b> project folder.</p>
 8 |       <form-entry label="Project URL" error="projectUrl">
 9 |         <input slot="field" class="textfield" type="text" v-model.trim="projectUrl" @keydown.enter="resolve()">
10 |         <div class="form-entry__info">
11 |           <b>Example:</b> {{config.token.serverUrl}}/path/to/project
12 |         </div>
13 |       </form-entry>
14 |       <form-entry label="Folder path" info="optional">
15 |         <input slot="field" class="textfield" type="text" v-model.trim="path" @keydown.enter="resolve()">
16 |         <div class="form-entry__info">
17 |           If not supplied, the root folder will be used.
18 |         </div>
19 |       </form-entry>
20 |       <form-entry label="Branch" info="optional">
21 |         <input slot="field" class="textfield" type="text" v-model.trim="branch" @keydown.enter="resolve()">
22 |         <div class="form-entry__info">
23 |           If not supplied, the <code>master</code> branch will be used.
24 |         </div>
25 |       </form-entry>
26 |     </div>
27 |     <div class="modal__button-bar">
28 |       <button class="button" @click="config.reject()">Cancel</button>
29 |       <button class="button button--resolve" @click="resolve()">Ok</button>
30 |     </div>
31 |   </modal-inner>
32 | </template>
33 | 
34 | <script>
35 | import utils from '../../../services/utils';
36 | import modalTemplate from '../common/modalTemplate';
37 | 
38 | export default modalTemplate({
39 |   data: () => ({
40 |     branch: '',
41 |     path: '',
42 |   }),
43 |   computedLocalSettings: {
44 |     projectUrl: 'gitlabWorkspaceProjectUrl',
45 |   },
46 |   methods: {
47 |     resolve() {
48 |       const projectPath = utils.parseGitlabProjectPath(this.projectUrl);
49 |       if (!projectPath) {
50 |         this.setError('projectUrl');
51 |       } else {
52 |         const path = this.path && this.path.replace(/^\//, '');
53 |         const url = utils.addQueryParams('app', {
54 |           providerId: 'gitlabWorkspace',
55 |           serverUrl: this.config.token.serverUrl,
56 |           projectPath,
57 |           branch: this.branch || 'master',
58 |           path: path || undefined,
59 |           sub: this.config.token.sub,
60 |         }, true);
61 |         this.config.resolve();
62 |         window.open(url);
63 |       }
64 |     },
65 |   },
66 | });
67 | </script>
68 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/GoogleDriveAccountModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Link Google Drive account">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="googleDrive"></icon-provider>
 6 |       </div>
 7 |       <p>Link your <b>Google Drive</b> account to <b>StackEdit</b>.</p>
 8 |       <div class="form-entry">
 9 |         <div class="form-entry__checkbox">
10 |           <label>
11 |             <input type="checkbox" v-model="restrictedAccess"> Restrict access
12 |           </label>
13 |           <div class="form-entry__info">
14 |             If checked, access will be restricted to files that you have opened or created with <b>StackEdit</b>.
15 |           </div>
16 |         </div>
17 |       </div>
18 |     </div>
19 |     <div class="modal__button-bar">
20 |       <button class="button" @click="config.reject()">Cancel</button>
21 |       <button class="button button--resolve" @click="config.resolve()">Ok</button>
22 |     </div>
23 |   </modal-inner>
24 | </template>
25 | 
26 | <script>
27 | import modalTemplate from '../common/modalTemplate';
28 | 
29 | export default modalTemplate({
30 |   computedLocalSettings: {
31 |     restrictedAccess: 'googleDriveRestrictedAccess',
32 |   },
33 | });
34 | </script>
35 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/GoogleDriveSaveModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Synchronize with Google Drive">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="googleDrive"></icon-provider>
 6 |       </div>
 7 |       <p>Save <b>{{currentFileName}}</b> to your <b>Google Drive</b> account and keep it synced.</p>
 8 |       <form-entry label="Folder ID" info="optional">
 9 |         <input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
10 |         <div class="form-entry__info">
11 |           If not supplied, the file will be created in your Drive root folder.
12 |         </div>
13 |         <div class="form-entry__actions">
14 |           <a href="javascript:void(0)" @click="openFolder">Choose folder</a>
15 |         </div>
16 |       </form-entry>
17 |       <form-entry label="Existing file ID" info="optional">
18 |         <input slot="field" class="textfield" type="text" v-model.trim="fileId" @keydown.enter="resolve()">
19 |         <div class="form-entry__info">
20 |           This will overwrite the file on the server.
21 |         </div>
22 |       </form-entry>
23 |     </div>
24 |     <div class="modal__button-bar">
25 |       <button class="button" @click="config.reject()">Cancel</button>
26 |       <button class="button button--resolve" @click="resolve()">Ok</button>
27 |     </div>
28 |   </modal-inner>
29 | </template>
30 | 
31 | <script>
32 | import googleHelper from '../../../services/providers/helpers/googleHelper';
33 | import googleDriveProvider from '../../../services/providers/googleDriveProvider';
34 | import modalTemplate from '../common/modalTemplate';
35 | import store from '../../../store';
36 | 
37 | export default modalTemplate({
38 |   data: () => ({
39 |     fileId: '',
40 |   }),
41 |   computedLocalSettings: {
42 |     folderId: 'googleDriveFolderId',
43 |   },
44 |   methods: {
45 |     openFolder() {
46 |       return store.dispatch(
47 |         'modal/hideUntil',
48 |         googleHelper.openPicker(this.config.token, 'folder')
49 |           .then((folders) => {
50 |             if (folders[0]) {
51 |               store.dispatch('data/patchLocalSettings', {
52 |                 googleDriveFolderId: folders[0].id,
53 |               });
54 |             }
55 |           }),
56 |       );
57 |     },
58 |     resolve() {
59 |       // Return new location
60 |       const location = googleDriveProvider.makeLocation(
61 |         this.config.token,
62 |         this.fileId,
63 |         this.folderId,
64 |       );
65 |       this.config.resolve(location);
66 |     },
67 |   },
68 | });
69 | </script>
70 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/GoogleDriveWorkspaceModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Add Google Drive workspace">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="googleDrive"></icon-provider>
 6 |       </div>
 7 |       <p>Create a workspace synced with a <b>Google Drive</b> folder.</p>
 8 |       <form-entry label="Folder ID" info="optional">
 9 |         <input slot="field" class="textfield" type="text" v-model.trim="folderId" @keydown.enter="resolve()">
10 |         <div class="form-entry__info">
11 |           If not supplied, a new workspace folder will be created in your Drive root folder.
12 |         </div>
13 |         <div class="form-entry__actions">
14 |           <a href="javascript:void(0)" @click="openFolder">Choose folder</a>
15 |         </div>
16 |       </form-entry>
17 |     </div>
18 |     <div class="modal__button-bar">
19 |       <button class="button" @click="config.reject()">Cancel</button>
20 |       <button class="button button--resolve" @click="resolve()">Ok</button>
21 |     </div>
22 |   </modal-inner>
23 | </template>
24 | 
25 | <script>
26 | import googleHelper from '../../../services/providers/helpers/googleHelper';
27 | import modalTemplate from '../common/modalTemplate';
28 | import utils from '../../../services/utils';
29 | import store from '../../../store';
30 | 
31 | export default modalTemplate({
32 |   computedLocalSettings: {
33 |     folderId: 'googleDriveWorkspaceFolderId',
34 |   },
35 |   methods: {
36 |     openFolder() {
37 |       return store.dispatch(
38 |         'modal/hideUntil',
39 |         googleHelper.openPicker(this.config.token, 'folder')
40 |           .then((folders) => {
41 |             if (folders[0]) {
42 |               store.dispatch('data/patchLocalSettings', {
43 |                 googleDriveWorkspaceFolderId: folders[0].id,
44 |               });
45 |             }
46 |           }),
47 |       );
48 |     },
49 |     resolve() {
50 |       const url = utils.addQueryParams('app', {
51 |         providerId: 'googleDriveWorkspace',
52 |         folderId: this.folderId,
53 |         sub: this.config.token.sub,
54 |       }, true);
55 |       this.config.resolve();
56 |       window.open(url);
57 |     },
58 |   },
59 | });
60 | </script>
61 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/GooglePhotoModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner class="modal__inner-1--google-photo" aria-label="Import Google Photo">
 3 |     <div class="modal__content">
 4 |       <div class="google-photo__tumbnail" :style="{backgroundImage: thumbnailUrl}"></div>
 5 |       <form-entry label="Title" info="optional">
 6 |         <input slot="field" class="textfield" type="text" v-model.trim="title" @keydown.enter="resolve()">
 7 |       </form-entry>
 8 |       <form-entry label="Size limit" info="optional">
 9 |         <input slot="field" class="textfield" type="text" v-model.trim="size" @keydown.enter="resolve()">
10 |       </form-entry>
11 |     </div>
12 |     <div class="modal__button-bar">
13 |       <button class="button" @click="reject()">Cancel</button>
14 |       <button class="button button--resolve" @click="resolve()">Ok</button>
15 |     </div>
16 |   </modal-inner>
17 | </template>
18 | 
19 | <script>
20 | import { mapGetters } from 'vuex';
21 | import ModalInner from '../common/ModalInner';
22 | import FormEntry from '../common/FormEntry';
23 | 
24 | const makeThumbnail = (url, size) => `${url}=s${size}`;
25 | 
26 | export default {
27 |   components: {
28 |     ModalInner,
29 |     FormEntry,
30 |   },
31 |   data: () => ({
32 |     title: '',
33 |     size: '',
34 |   }),
35 |   computed: {
36 |     thumbnailUrl() {
37 |       return `url(${makeThumbnail(this.config.url, 320)})`;
38 |     },
39 |     ...mapGetters('modal', [
40 |       'config',
41 |     ]),
42 |   },
43 |   methods: {
44 |     resolve() {
45 |       let { url } = this.config;
46 |       const size = parseInt(this.size, 10);
47 |       if (!Number.isNaN(size)) {
48 |         url = makeThumbnail(url, size);
49 |       }
50 |       if (this.title) {
51 |         url += ` "${this.title}"`;
52 |       }
53 |       const { callback } = this.config;
54 |       this.config.resolve();
55 |       callback(url);
56 |     },
57 |     reject() {
58 |       const { callback } = this.config;
59 |       this.config.reject();
60 |       callback(null);
61 |     },
62 |   },
63 | };
64 | </script>
65 | 
66 | <style lang="scss">
67 | .google-photo__tumbnail {
68 |   height: 160px;
69 |   background-position: center;
70 |   background-repeat: no-repeat;
71 |   background-size: contain;
72 | }
73 | </style>
74 | 


--------------------------------------------------------------------------------
/src/components/modals/providers/ZendeskAccountModal.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <modal-inner aria-label="Link Zendesk account">
 3 |     <div class="modal__content">
 4 |       <div class="modal__image">
 5 |         <icon-provider provider-id="zendesk"></icon-provider>
 6 |       </div>
 7 |       <p>Link your <b>Zendesk</b> account to <b>StackEdit</b>.</p>
 8 |       <form-entry label="Site URL" error="siteUrl">
 9 |         <input slot="field" class="textfield" type="text" v-model.trim="siteUrl" @keydown.enter="resolve()">
10 |         <div class="form-entry__info">
11 |           <b>Example:</b> https://example.zendesk.com/
12 |         </div>
13 |       </form-entry>
14 |       <form-entry label="Client Unique Identifier" error="clientId">
15 |         <input slot="field" class="textfield" type="text" v-model.trim="clientId" @keydown.enter="resolve()">
16 |         <div class="form-entry__info">
17 |           You have to configure an OAuth Client with redirect URL <b>{{redirectUrl}}</b>
18 |         </div>
19 |         <div class="form-entry__actions">
20 |           <a href="https://support.zendesk.com/hc/en-us/articles/203663836" target="_blank">More info</a>
21 |         </div>
22 |       </form-entry>
23 |     </div>
24 |     <div class="modal__button-bar">
25 |       <button class="button" @click="config.reject()">Cancel</button>
26 |       <button class="button button--resolve" @click="resolve()">Ok</button>
27 |     </div>
28 |   </modal-inner>
29 | </template>
30 | 
31 | <script>
32 | import modalTemplate from '../common/modalTemplate';
33 | import constants from '../../../data/constants';
34 | 
35 | export default modalTemplate({
36 |   data: () => ({
37 |     redirectUrl: constants.oauth2RedirectUri,
38 |   }),
39 |   computedLocalSettings: {
40 |     siteUrl: 'zendeskSiteUrl',
41 |     clientId: 'zendeskClientId',
42 |   },
43 |   methods: {
44 |     resolve() {
45 |       if (!this.siteUrl) {
46 |         this.setError('siteUrl');
47 |       }
48 |       if (!this.clientId) {
49 |         this.setError('clientId');
50 |       }
51 |       if (this.siteUrl && this.clientId) {
52 |         const parsedUrl = this.siteUrl.match(/^https:\/\/([^.]+)\.zendesk\.com/);
53 |         if (!parsedUrl) {
54 |           this.setError('siteUrl');
55 |         } else {
56 |           this.config.resolve({
57 |             subdomain: parsedUrl[1],
58 |             clientId: this.clientId,
59 |           });
60 |         }
61 |       }
62 |     },
63 |   },
64 | });
65 | </script>
66 | 


--------------------------------------------------------------------------------
/src/data/constants.js:
--------------------------------------------------------------------------------
 1 | const origin = `${window.location.protocol}//${window.location.host}`;
 2 | 
 3 | export default {
 4 |   cleanTrashAfter: 7 * 24 * 60 * 60 * 1000, // 7 days
 5 |   origin,
 6 |   oauth2RedirectUri: `${origin}/oauth2/callback`,
 7 |   types: [
 8 |     'contentState',
 9 |     'syncedContent',
10 |     'content',
11 |     'file',
12 |     'folder',
13 |     'syncLocation',
14 |     'publishLocation',
15 |     'data',
16 |   ],
17 |   localStorageDataIds: [
18 |     'workspaces',
19 |     'settings',
20 |     'layoutSettings',
21 |     'tokens',
22 |     'badgeCreations',
23 |     'serverConf',
24 |   ],
25 |   textMaxLength: 250000,
26 |   defaultName: 'Untitled',
27 | };
28 | 


--------------------------------------------------------------------------------
/src/data/defaults/defaultLayoutSettings.js:
--------------------------------------------------------------------------------
 1 | export default () => ({
 2 |   showNavigationBar: true,
 3 |   showEditor: true,
 4 |   showSidePreview: true,
 5 |   showStatusBar: true,
 6 |   showSideBar: false,
 7 |   showExplorer: false,
 8 |   scrollSync: true,
 9 |   focusMode: false,
10 |   findCaseSensitive: false,
11 |   findUseRegexp: false,
12 |   sideBarPanel: 'menu',
13 |   welcomeTourFinished: false,
14 | });
15 | 


--------------------------------------------------------------------------------
/src/data/defaults/defaultLocalSettings.js:
--------------------------------------------------------------------------------
 1 | export default () => ({
 2 |   welcomeFileHashes: {},
 3 |   filePropertiesTab: '',
 4 |   htmlExportTemplate: 'styledHtml',
 5 |   pdfExportTemplate: 'styledHtml',
 6 |   pandocExportFormat: 'pdf',
 7 |   googleDriveRestrictedAccess: false,
 8 |   googleDriveFolderId: '',
 9 |   googleDriveWorkspaceFolderId: '',
10 |   googleDrivePublishFormat: 'markdown',
11 |   googleDrivePublishTemplate: 'styledHtml',
12 |   bloggerBlogUrl: '',
13 |   bloggerPublishTemplate: 'plainHtml',
14 |   dropboxRestrictedAccess: false,
15 |   dropboxPublishTemplate: 'styledHtml',
16 |   githubRepoFullAccess: false,
17 |   githubRepoUrl: '',
18 |   githubWorkspaceRepoUrl: '',
19 |   githubPublishTemplate: 'jekyllSite',
20 |   gistIsPublic: false,
21 |   gistPublishTemplate: 'plainText',
22 |   gitlabServerUrl: '',
23 |   gitlabApplicationId: '',
24 |   gitlabProjectUrl: '',
25 |   gitlabWorkspaceProjectUrl: '',
26 |   gitlabPublishTemplate: 'plainText',
27 |   wordpressDomain: '',
28 |   wordpressPublishTemplate: 'plainHtml',
29 |   zendeskSiteUrl: '',
30 |   zendeskClientId: '',
31 |   zendescPublishSectionId: '',
32 |   zendescPublishLocale: '',
33 |   zendeskPublishTemplate: 'plainHtml',
34 | });
35 | 


--------------------------------------------------------------------------------
/src/data/defaults/defaultSettings.yml:
--------------------------------------------------------------------------------
 1 | # light or dark
 2 | colorTheme: light
 3 | # Adjust font size in editor and preview
 4 | fontSizeFactor: 1
 5 | # Adjust maximum text width in editor and preview
 6 | maxWidthFactor: 1
 7 | # Auto-sync frequency (in ms). Minimum is 60000.
 8 | autoSyncEvery: 90000
 9 | 
10 | # Editor settings
11 | editor:
12 |   # Automatic list numbering
13 |   listAutoNumber: true
14 |   # Display images in the editor
15 |   inlineImages: true
16 |   # Use monospaced font only
17 |   monospacedFontOnly: false
18 | 
19 | # Keyboard shortcuts
20 | # See https://craig.is/killing/mice
21 | shortcuts:
22 |   mod+s: sync
23 |   mod+f: find
24 |   mod+alt+f: replace
25 |   mod+g: replace
26 |   mod+shift+b: bold
27 |   mod+shift+c: clist
28 |   mod+shift+k: code
29 |   mod+shift+h: heading
30 |   mod+shift+r: hr
31 |   mod+shift+g: image
32 |   mod+shift+i: italic
33 |   mod+shift+l: link
34 |   mod+shift+o: olist
35 |   mod+shift+q: quote
36 |   mod+shift+s: strikethrough
37 |   mod+shift+t: table
38 |   mod+shift+u: ulist
39 |   '= = > space':
40 |     method: expand
41 |     params:
42 |     - '==> '
43 |     - '⇒ '
44 |   '< = = space':
45 |     method: expand
46 |     params:
47 |     - '<== '
48 |     - '⇐ '
49 | 
50 | # Options passed to wkhtmltopdf
51 | # See https://wkhtmltopdf.org/usage/wkhtmltopdf.txt
52 | wkhtmltopdf:
53 |   marginTop: 25
54 |   marginRight: 25
55 |   marginBottom: 25
56 |   marginLeft: 25
57 |   # A3, A4, Legal or Letter
58 |   pageSize: A4
59 | 
60 | # Options passed to pandoc
61 | # See https://pandoc.org/MANUAL.html
62 | pandoc:
63 |   highlightStyle: kate
64 |   toc: true
65 |   tocDepth: 3
66 | 
67 | # HTML to Markdown converter options
68 | # See https://github.com/domchristie/turndown
69 | turndown:
70 |   headingStyle: atx
71 |   hr: ----------
72 |   bulletListMarker: '-'
73 |   codeBlockStyle: fenced
74 |   fence: '```'
75 |   emDelimiter: _
76 |   strongDelimiter: '**'
77 |   linkStyle: inlined
78 |   linkReferenceStyle: full
79 | 
80 | # GitHub/GitLab commit messages
81 | git:
82 |   createFileMessage: '{{path}} created from https://stackedit.io/'
83 |   updateFileMessage: '{{path}} updated from https://stackedit.io/'
84 |   deleteFileMessage: '{{path}} deleted from https://stackedit.io/'
85 | 
86 | # Default content for new files
87 | newFileContent: |
88 | 
89 | 
90 | 
91 |   > Written with [StackEdit](https://stackedit.io/).
92 | 
93 | # Default properties for new files
94 | newFileProperties: |
95 | #  extensions:
96 | #    preset: gfm
97 | 
98 | 


--------------------------------------------------------------------------------
/src/data/defaults/defaultWorkspaces.js:
--------------------------------------------------------------------------------
1 | export default () => ({
2 |   main: {
3 |     id: 'main',
4 |     name: 'Main workspace',
5 |     // The rest will be filled by the workspace/workspacesById getter
6 |   },
7 | });
8 | 


--------------------------------------------------------------------------------
/src/data/empties/emptyContent.js:
--------------------------------------------------------------------------------
 1 | export default (id = null) => ({
 2 |   id,
 3 |   type: 'content',
 4 |   text: '\n',
 5 |   properties: '\n',
 6 |   discussions: {},
 7 |   comments: {},
 8 |   hash: 0,
 9 | });
10 | 


--------------------------------------------------------------------------------
/src/data/empties/emptyContentState.js:
--------------------------------------------------------------------------------
1 | export default (id = null) => ({
2 |   id,
3 |   type: 'contentState',
4 |   selectionStart: 0,
5 |   selectionEnd: 0,
6 |   scrollPosition: null,
7 |   hash: 0,
8 | });
9 | 


--------------------------------------------------------------------------------
/src/data/empties/emptyFile.js:
--------------------------------------------------------------------------------
1 | export default (id = null) => ({
2 |   id,
3 |   type: 'file',
4 |   name: '',
5 |   parentId: null,
6 |   hash: 0,
7 | });
8 | 


--------------------------------------------------------------------------------
/src/data/empties/emptyFolder.js:
--------------------------------------------------------------------------------
1 | export default (id = null) => ({
2 |   id,
3 |   type: 'folder',
4 |   name: '',
5 |   parentId: null,
6 |   hash: 0,
7 | });
8 | 


--------------------------------------------------------------------------------
/src/data/empties/emptyPublishLocation.js:
--------------------------------------------------------------------------------
1 | export default (id = null) => ({
2 |   id,
3 |   type: 'publishLocation',
4 |   providerId: null,
5 |   fileId: null,
6 |   templateId: null,
7 |   hash: 0,
8 | });
9 | 


--------------------------------------------------------------------------------
/src/data/empties/emptySyncLocation.js:
--------------------------------------------------------------------------------
1 | export default (id = null) => ({
2 |   id,
3 |   type: 'syncLocation',
4 |   providerId: null,
5 |   fileId: null,
6 |   hash: 0,
7 | });
8 | 


--------------------------------------------------------------------------------
/src/data/empties/emptySyncedContent.js:
--------------------------------------------------------------------------------
1 | export default (id = null) => ({
2 |   id,
3 |   type: 'syncedContent',
4 |   historyData: {},
5 |   syncHistory: {},
6 |   v: 0,
7 |   hash: 0,
8 | });
9 | 


--------------------------------------------------------------------------------
/src/data/empties/emptyTemplateHelpers.js:
--------------------------------------------------------------------------------
 1 | /* Add your custom Handlebars helpers here.
 2 | 
 3 | For example:
 4 | 
 5 | Handlebars.registerHelper('transform', function (options) {
 6 |   var result = options.fn(this);
 7 |   return new Handlebars.SafeString(
 8 |     result.replace(/<pre[^>]*>/g, '<pre class="prettyprint">')
 9 |   );
10 | });
11 | 
12 | Then use the helper in your template:
13 | 
14 | {{#transform}}{{{files.0.content.html}}}{{/transform}}
15 | */
16 | 
17 | 


--------------------------------------------------------------------------------
/src/data/empties/emptyTemplateValue.html:
--------------------------------------------------------------------------------
 1 | <!-- Specify your Handlebars template here.
 2 | 
 3 | The following JavaScript context will be passed to the template:
 4 | 
 5 | {
 6 |   files: [{
 7 |     name: 'The filename',
 8 |     content: {
 9 |       text: 'The file content',
10 |       html: '<p>The file content</p>',
11 |       yamlProperties: 'The file properties in YAML format',
12 |       properties: {
13 |         // Computed file properties object
14 |       },
15 |       toc: [
16 |         // Table Of Contents tree
17 |       ]
18 |     }
19 |   }]
20 | }
21 | 
22 | 
23 | As an example:
24 | 
25 | <html><body>{{{files.0.content.html}}}</body></html>
26 | 
27 | will produce:
28 | 
29 | <html><body><p>The file content</p></body></html>
30 | 
31 | 
32 | You can use Handlebars built-in helpers and the custom StackEdit ones:
33 | 
34 | {{#tocToHtml files.0.content.toc}}{{/tocToHtml}} will produce a clickable TOC.
35 | 
36 | {{#tocToHtml files.0.content.toc 3}}{{/tocToHtml}} will limit the TOC depth to 3.
37 | -->
38 | 
39 | 


--------------------------------------------------------------------------------
/src/data/faq.md:
--------------------------------------------------------------------------------
 1 | **Where is my data stored?**
 2 | 
 3 | If your workspace is not synced, your files are stored inside your browser and nowhere else.
 4 | 
 5 | We recommend syncing your workspace to make sure files won't be lost in case your browser data is cleared. Self-hosted CouchDB or GitLab backends are well suited for privacy.
 6 | 
 7 | **Can StackEdit access my data without telling me?**
 8 | 
 9 | StackEdit is a browser-based application. The access tokens issued by Google, Dropbox, GitHub... are stored in your browser and are not sent to any kind of backend or 3^rd^ party so your data won't be accessed by anyone.
10 | 


--------------------------------------------------------------------------------
/src/data/markdownSample.md:
--------------------------------------------------------------------------------
  1 | Headers
  2 | ---------------------------
  3 | 
  4 | # Header 1
  5 | 
  6 | ## Header 2
  7 | 
  8 | ### Header 3
  9 | 
 10 | 
 11 | 
 12 | Styling
 13 | ---------------------------
 14 | 
 15 | *Emphasize* _emphasize_
 16 | 
 17 | **Strong** __strong__
 18 | 
 19 | ==Marked text.==
 20 | 
 21 | ~~Mistaken text.~~
 22 | 
 23 | > Quoted text.
 24 | 
 25 | H~2~O is a liquid.
 26 | 
 27 | 2^10^ is 1024.
 28 | 
 29 | 
 30 | 
 31 | Lists
 32 | ---------------------------
 33 | 
 34 | - Item
 35 |   * Item
 36 |     + Item
 37 | 
 38 | 1. Item 1
 39 | 2. Item 2
 40 | 3. Item 3
 41 | 
 42 | - [ ] Incomplete item
 43 | - [x] Complete item
 44 | 
 45 | 
 46 | 
 47 | Links
 48 | ---------------------------
 49 | 
 50 | A [link](http://example.com).
 51 | 
 52 | An image: ![Alt](img.jpg)
 53 | 
 54 | A sized image: ![Alt](img.jpg =60x50)
 55 | 
 56 | 
 57 | 
 58 | Code
 59 | ---------------------------
 60 | 
 61 | Some `inline code`.
 62 | 
 63 | ```
 64 | // A code block
 65 | var foo = 'bar';
 66 | ```
 67 | 
 68 | ```javascript
 69 | // An highlighted block
 70 | var foo = 'bar';
 71 | ```
 72 | 
 73 | 
 74 | 
 75 | Tables
 76 | ---------------------------
 77 | 
 78 | Item     | Value
 79 | -------- | -----
 80 | Computer | $1600
 81 | Phone    | $12
 82 | Pipe     | $1
 83 | 
 84 | 
 85 | | Column 1 | Column 2      |
 86 | |:--------:| -------------:|
 87 | | centered | right-aligned |
 88 | 
 89 | 
 90 | 
 91 | Definition lists
 92 | ---------------------------
 93 | 
 94 | Markdown
 95 | :  Text-to-HTML conversion tool
 96 | 
 97 | Authors
 98 | :  John
 99 | :  Luke
100 | 
101 | 
102 | 
103 | Footnotes
104 | ---------------------------
105 | 
106 | Some text with a footnote.[^1]
107 | 
108 | [^1]: The footnote.
109 | 
110 | 
111 | 
112 | Abbreviations
113 | ---------------------------
114 | 
115 | Markdown converts text to HTML.
116 | 
117 | *[HTML]: HyperText Markup Language
118 | 
119 | 
120 | 
121 | LaTeX math
122 | ---------------------------
123 | 
124 | The Gamma function satisfying $\Gamma(n) = (n-1)!\quad\forall
125 | n\in\mathbb N$ is via the Euler integral
126 | 
127 | $
128 | \Gamma(z) = \int_0^\infty t^{z-1}e^{-t}dt\,.
129 | $
130 | 


--------------------------------------------------------------------------------
/src/data/pagedownButtons.js:
--------------------------------------------------------------------------------
 1 | export default [{
 2 | }, {
 3 |   method: 'bold',
 4 |   title: 'Bold',
 5 |   icon: 'format-bold',
 6 | }, {
 7 |   method: 'italic',
 8 |   title: 'Italic',
 9 |   icon: 'format-italic',
10 | }, {
11 |   method: 'heading',
12 |   title: 'Heading',
13 |   icon: 'format-size',
14 | }, {
15 |   method: 'strikethrough',
16 |   title: 'Strikethrough',
17 |   icon: 'format-strikethrough',
18 | }, {
19 | }, {
20 |   method: 'ulist',
21 |   title: 'Unordered list',
22 |   icon: 'format-list-bulleted',
23 | }, {
24 |   method: 'olist',
25 |   title: 'Ordered list',
26 |   icon: 'format-list-numbers',
27 | }, {
28 |   method: 'clist',
29 |   title: 'Check list',
30 |   icon: 'format-list-checks',
31 | }, {
32 | }, {
33 |   method: 'quote',
34 |   title: 'Blockquote',
35 |   icon: 'format-quote-close',
36 | }, {
37 |   method: 'code',
38 |   title: 'Code',
39 |   icon: 'code-tags',
40 | }, {
41 |   method: 'table',
42 |   title: 'Table',
43 |   icon: 'table',
44 | }, {
45 |   method: 'link',
46 |   title: 'Link',
47 |   icon: 'link-variant',
48 | }, {
49 |   method: 'image',
50 |   title: 'Image',
51 |   icon: 'file-image',
52 | }];
53 | 


--------------------------------------------------------------------------------
/src/data/presets.js:
--------------------------------------------------------------------------------
  1 | const zero = {
  2 |   // Markdown extensions
  3 |   markdown: {
  4 |     abbr: false,
  5 |     breaks: false,
  6 |     deflist: false,
  7 |     del: false,
  8 |     fence: false,
  9 |     footnote: false,
 10 |     imgsize: false,
 11 |     linkify: false,
 12 |     mark: false,
 13 |     sub: false,
 14 |     sup: false,
 15 |     table: false,
 16 |     tasklist: false,
 17 |     typographer: false,
 18 |   },
 19 |   // Emoji extension
 20 |   emoji: {
 21 |     enabled: false,
 22 |     // Enable shortcuts like :) :-(
 23 |     shortcuts: false,
 24 |   },
 25 |   /*
 26 |   ABC Notation extension
 27 |   Render abc-notation code blocks to music sheets
 28 |   See https://abcjs.net/
 29 |   */
 30 |   abc: {
 31 |     enabled: false,
 32 |   },
 33 |   /*
 34 |   Katex extension
 35 |   Render LaTeX mathematical expressions using:
 36 |     $...$ for inline formulas
 37 |     $...$ for displayed formulas.
 38 |   See https://math.meta.stackexchange.com/questions/5020
 39 |   */
 40 |   katex: {
 41 |     enabled: false,
 42 |   },
 43 |   /*
 44 |   Mermaid extension
 45 |   Convert code blocks starting with ```mermaid
 46 |   into diagrams and flowcharts.
 47 |   See https://mermaidjs.github.io/
 48 |   */
 49 |   mermaid: {
 50 |     enabled: false,
 51 |   },
 52 | };
 53 | 
 54 | export default {
 55 |   zero: [zero],
 56 |   commonmark: [zero, {
 57 |     markdown: {
 58 |       fence: true,
 59 |     },
 60 |   }],
 61 |   gfm: [zero, {
 62 |     markdown: {
 63 |       breaks: true,
 64 |       del: true,
 65 |       fence: true,
 66 |       linkify: true,
 67 |       table: true,
 68 |       tasklist: true,
 69 |     },
 70 |     emoji: {
 71 |       enabled: true,
 72 |     },
 73 |   }],
 74 |   default: [zero, {
 75 |     markdown: {
 76 |       abbr: true,
 77 |       breaks: true,
 78 |       deflist: true,
 79 |       del: true,
 80 |       fence: true,
 81 |       footnote: true,
 82 |       imgsize: true,
 83 |       linkify: true,
 84 |       mark: true,
 85 |       sub: true,
 86 |       sup: true,
 87 |       table: true,
 88 |       tasklist: true,
 89 |       typographer: true,
 90 |     },
 91 |     emoji: {
 92 |       enabled: true,
 93 |     },
 94 |     katex: {
 95 |       enabled: true,
 96 |     },
 97 |     mermaid: {
 98 |       enabled: true,
 99 |     },
100 |     abc: {
101 |       enabled: true,
102 |     },
103 |   }],
104 | };
105 | 


--------------------------------------------------------------------------------
/src/data/templates/jekyllSiteTemplate.html:
--------------------------------------------------------------------------------
1 | ---
2 | {{{files.0.content.yamlProperties}}}
3 | ---
4 | 
5 | {{{files.0.content.html}}}
6 | 


--------------------------------------------------------------------------------
/src/data/templates/plainHtmlTemplate.html:
--------------------------------------------------------------------------------
1 | {{{files.0.content.html}}}
2 | 


--------------------------------------------------------------------------------
/src/data/templates/styledHtmlTemplate.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html>
 3 | 
 4 | <head>
 5 |   <meta charset="utf-8">
 6 |   <meta name="viewport" content="width=device-width, initial-scale=1.0">
 7 |   <title>{{files.0.name}}</title>
 8 |   <link rel="stylesheet" href="https://stackedit.io/style.css" />
 9 | </head>
10 | 
11 | {{#if pdf}}
12 | <body class="stackedit stackedit--pdf">
13 | {{else}}
14 | <body class="stackedit">
15 | {{/if}}
16 |   <div class="stackedit__html">{{{files.0.content.html}}}</div>
17 | </body>
18 | 
19 | </html>
20 | 


--------------------------------------------------------------------------------
/src/data/templates/styledHtmlWithTocTemplate.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html>
 3 | 
 4 | <head>
 5 |   <meta charset="utf-8">
 6 |   <meta name="viewport" content="width=device-width, initial-scale=1.0">
 7 |   <title>{{files.0.name}}</title>
 8 |   <link rel="stylesheet" href="https://stackedit.io/style.css" />
 9 | </head>
10 | 
11 | {{#if pdf}}
12 | <body class="stackedit stackedit--pdf">
13 | {{else}}
14 | <body class="stackedit">
15 | {{/if}}
16 |   <div class="stackedit__left">
17 |     <div class="stackedit__toc">
18 |       {{#tocToHtml files.0.content.toc 2}}{{/tocToHtml}}
19 |     </div>
20 |   </div>
21 |   <div class="stackedit__right">
22 |     <div class="stackedit__html">
23 |       {{{files.0.content.html}}}
24 |     </div>
25 |   </div>
26 | </body>
27 | 
28 | </html>
29 | 


--------------------------------------------------------------------------------
/src/extensions/abcExtension.js:
--------------------------------------------------------------------------------
 1 | import renderAbc from 'abcjs/src/api/abc_tunebook_svg';
 2 | import extensionSvc from '../services/extensionSvc';
 3 | 
 4 | const render = (elt) => {
 5 |   const content = elt.textContent;
 6 |   // Create a div element
 7 |   const divElt = document.createElement('div');
 8 |   divElt.className = 'abc-notation-block';
 9 |   // Replace the pre element with the div
10 |   elt.parentNode.parentNode.replaceChild(divElt, elt.parentNode);
11 |   renderAbc(divElt, content, {});
12 | };
13 | 
14 | extensionSvc.onGetOptions((options, properties) => {
15 |   options.abc = properties.extensions.abc.enabled;
16 | });
17 | 
18 | extensionSvc.onSectionPreview((elt) => {
19 |   elt.querySelectorAll('.prism.language-abc')
20 |     .cl_each(notationElt => render(notationElt));
21 | });
22 | 


--------------------------------------------------------------------------------
/src/extensions/emojiExtension.js:
--------------------------------------------------------------------------------
 1 | import markdownItEmoji from 'markdown-it-emoji';
 2 | import extensionSvc from '../services/extensionSvc';
 3 | 
 4 | extensionSvc.onGetOptions((options, properties) => {
 5 |   options.emoji = properties.extensions.emoji.enabled;
 6 |   options.emojiShortcuts = properties.extensions.emoji.shortcuts;
 7 | });
 8 | 
 9 | extensionSvc.onInitConverter(1, (markdown, options) => {
10 |   if (options.emoji) {
11 |     markdown.use(markdownItEmoji, options.emojiShortcuts ? {} : { shortcuts: {} });
12 |   }
13 | });
14 | 


--------------------------------------------------------------------------------
/src/extensions/index.js:
--------------------------------------------------------------------------------
1 | import './emojiExtension';
2 | import './abcExtension';
3 | import './katexExtension';
4 | import './markdownExtension';
5 | import './mermaidExtension';
6 | 


--------------------------------------------------------------------------------
/src/extensions/katexExtension.js:
--------------------------------------------------------------------------------
 1 | import katex from 'katex';
 2 | import markdownItMath from './libs/markdownItMath';
 3 | import extensionSvc from '../services/extensionSvc';
 4 | 
 5 | extensionSvc.onGetOptions((options, properties) => {
 6 |   options.math = properties.extensions.katex.enabled;
 7 | });
 8 | 
 9 | extensionSvc.onInitConverter(2, (markdown, options) => {
10 |   if (options.math) {
11 |     markdown.use(markdownItMath);
12 |     markdown.renderer.rules.inline_math = (tokens, idx) =>
13 |       `<span class="katex--inline">${markdown.utils.escapeHtml(tokens[idx].content)}</span>`;
14 |     markdown.renderer.rules.display_math = (tokens, idx) =>
15 |       `<span class="katex--display">${markdown.utils.escapeHtml(tokens[idx].content)}</span>`;
16 |   }
17 | });
18 | 
19 | extensionSvc.onSectionPreview((elt) => {
20 |   const highlighter = displayMode => (katexElt) => {
21 |     if (!katexElt.highlighted) {
22 |       try {
23 |         katex.render(katexElt.textContent, katexElt, { displayMode });
24 |       } catch (e) {
25 |         katexElt.textContent = `${e.message}`;
26 |       }
27 |     }
28 |     katexElt.highlighted = true;
29 |   };
30 |   elt.querySelectorAll('.katex--inline').cl_each(highlighter(false));
31 |   elt.querySelectorAll('.katex--display').cl_each(highlighter(true));
32 | });
33 | 


--------------------------------------------------------------------------------
/src/extensions/libs/markdownItAnchor.js:
--------------------------------------------------------------------------------
 1 | export default (md) => {
 2 |   md.core.ruler.before('replacements', 'anchors', (state) => {
 3 |     const anchorHash = {};
 4 |     let headingOpenToken;
 5 |     let headingContent;
 6 |     state.tokens.forEach((token) => {
 7 |       if (token.type === 'heading_open') {
 8 |         headingContent = '';
 9 |         headingOpenToken = token;
10 |       } else if (token.type === 'heading_close') {
11 |         headingOpenToken.headingContent = headingContent;
12 | 
13 |         // According to http://pandoc.org/README.html#extension-auto_identifiers
14 |         let slug = headingContent
15 |           .replace(/\s/g, '-') // Replace all spaces and newlines with hyphens
16 |           .replace(/[\0-,/:-@[-^`{-~]/g, '') // Remove all punctuation, except underscores, hyphens, and periods
17 |           .toLowerCase(); // Convert all alphabetic characters to lowercase
18 | 
19 |         // Remove everything up to the first letter
20 |         let i;
21 |         for (i = 0; i < slug.length; i += 1) {
22 |           const charCode = slug.charCodeAt(i);
23 |           if ((charCode >= 0x61 && charCode <= 0x7A) || charCode > 0x7E) {
24 |             break;
25 |           }
26 |         }
27 | 
28 |         // If nothing left after this, use `section`
29 |         slug = slug.slice(i) || 'section';
30 | 
31 |         let anchor = slug;
32 |         let index = 1;
33 |         while (Object.prototype.hasOwnProperty.call(anchorHash, anchor)) {
34 |           anchor = `${slug}-${index}`;
35 |           index += 1;
36 |         }
37 |         anchorHash[anchor] = true;
38 |         headingOpenToken.headingAnchor = anchor;
39 |         headingOpenToken.attrs = [
40 |           ['id', anchor],
41 |         ];
42 |         headingOpenToken = undefined;
43 |       } else if (headingOpenToken) {
44 |         headingContent += token.children.reduce((result, child) => {
45 |           if (child.type !== 'footnote_ref') {
46 |             return result + child.content;
47 |           }
48 |           return result;
49 |         }, '');
50 |       }
51 |     });
52 |   });
53 | };
54 | 


--------------------------------------------------------------------------------
/src/extensions/libs/markdownItMath.js:
--------------------------------------------------------------------------------
 1 | function texMath(state, silent) {
 2 |   let startMathPos = state.pos;
 3 |   if (state.src.charCodeAt(startMathPos) !== 0x24 /* $ */) {
 4 |     return false;
 5 |   }
 6 | 
 7 |   // Parse tex math according to http://pandoc.org/README.html#math
 8 |   let endMarker = '
#39;;
 9 |   startMathPos += 1;
10 |   const afterStartMarker = state.src.charCodeAt(startMathPos);
11 |   if (afterStartMarker === 0x24 /* $ */) {
12 |     endMarker = '$';
13 |     startMathPos += 1;
14 |     if (state.src.charCodeAt(startMathPos) === 0x24 /* $ */) {
15 |       // 3 markers are too much
16 |       return false;
17 |     }
18 |   } else if (
19 |     // Skip if opening $ is succeeded by a space character
20 |     afterStartMarker === 0x20 /* space */
21 |     || afterStartMarker === 0x09 /* \t */
22 |     || afterStartMarker === 0x0a /* \n */
23 |   ) {
24 |     return false;
25 |   }
26 |   const endMarkerPos = state.src.indexOf(endMarker, startMathPos);
27 |   if (endMarkerPos === -1) {
28 |     return false;
29 |   }
30 |   if (state.src.charCodeAt(endMarkerPos - 1) === 0x5C /* \ */) {
31 |     return false;
32 |   }
33 |   const nextPos = endMarkerPos + endMarker.length;
34 |   if (endMarker.length === 1) {
35 |     // Skip if $ is preceded by a space character
36 |     const beforeEndMarker = state.src.charCodeAt(endMarkerPos - 1);
37 |     if (beforeEndMarker === 0x20 /* space */
38 |       || beforeEndMarker === 0x09 /* \t */
39 |       || beforeEndMarker === 0x0a /* \n */) {
40 |       return false;
41 |     }
42 |     // Skip if closing $ is succeeded by a digit (eg $5 $10 ...)
43 |     const suffix = state.src.charCodeAt(nextPos);
44 |     if (suffix >= 0x30 && suffix < 0x3A) {
45 |       return false;
46 |     }
47 |   }
48 | 
49 |   if (!silent) {
50 |     const token = state.push(endMarker.length === 1 ? 'inline_math' : 'display_math', '', 0);
51 |     token.content = state.src.slice(startMathPos, endMarkerPos);
52 |   }
53 |   state.pos = nextPos;
54 |   return true;
55 | }
56 | 
57 | export default (md) => {
58 |   md.inline.ruler.push('texMath', texMath);
59 | };
60 | 


--------------------------------------------------------------------------------
/src/extensions/libs/markdownItTasklist.js:
--------------------------------------------------------------------------------
 1 | function attrSet(token, name, value) {
 2 |   const index = token.attrIndex(name);
 3 |   const attr = [name, value];
 4 | 
 5 |   if (index < 0) {
 6 |     token.attrPush(attr);
 7 |   } else {
 8 |     token.attrs[index] = attr;
 9 |   }
10 | }
11 | 
12 | module.exports = (md) => {
13 |   md.core.ruler.after('inline', 'tasklist', ({ tokens, Token }) => {
14 |     for (let i = 2; i < tokens.length; i += 1) {
15 |       const token = tokens[i];
16 |       if (token.content
17 |         && token.content.charCodeAt(0) === 0x5b /* [ */
18 |         && token.content.charCodeAt(2) === 0x5d /* ] */
19 |         && token.content.charCodeAt(3) === 0x20 /* space */
20 |         && token.type === 'inline'
21 |         && tokens[i - 1].type === 'paragraph_open'
22 |         && tokens[i - 2].type === 'list_item_open'
23 |       ) {
24 |         const cross = token.content[1].toLowerCase();
25 |         if (cross === ' ' || cross === 'x') {
26 |           const checkbox = new Token('html_inline', '', 0);
27 |           if (cross === ' ') {
28 |             checkbox.content = '<span class="task-list-item-checkbox" type="checkbox">&#9744;</span>';
29 |           } else {
30 |             checkbox.content = '<span class="task-list-item-checkbox checked" type="checkbox">&#9745;</span>';
31 |           }
32 |           token.children.unshift(checkbox);
33 |           token.children[1].content = token.children[1].content.slice(3);
34 |           token.content = token.content.slice(3);
35 |           attrSet(tokens[i - 2], 'class', 'task-list-item');
36 |         }
37 |       }
38 |     }
39 |   });
40 | };
41 | 


--------------------------------------------------------------------------------
/src/extensions/mermaidExtension.js:
--------------------------------------------------------------------------------
 1 | import 'mermaid';
 2 | import extensionSvc from '../services/extensionSvc';
 3 | import utils from '../services/utils';
 4 | 
 5 | const config = {
 6 |   logLevel: 5,
 7 |   startOnLoad: false,
 8 |   arrowMarkerAbsolute: false,
 9 |   theme: 'neutral',
10 |   flowchart: {
11 |     htmlLabels: true,
12 |     curve: 'linear',
13 |   },
14 |   sequence: {
15 |     diagramMarginX: 50,
16 |     diagramMarginY: 10,
17 |     actorMargin: 50,
18 |     width: 150,
19 |     height: 65,
20 |     boxMargin: 10,
21 |     boxTextMargin: 5,
22 |     noteMargin: 10,
23 |     messageMargin: 35,
24 |     mirrorActors: true,
25 |     bottomMarginAdj: 1,
26 |     useMaxWidth: true,
27 |   },
28 |   gantt: {
29 |     titleTopMargin: 25,
30 |     barHeight: 20,
31 |     barGap: 4,
32 |     topPadding: 50,
33 |     leftPadding: 75,
34 |     gridLineStartPadding: 35,
35 |     fontSize: 11,
36 |     fontFamily: '"Open-Sans", "sans-serif"',
37 |     numberSectionStyles: 4,
38 |     axisFormat: '%Y-%m-%d',
39 |   },
40 | };
41 | 
42 | const containerElt = document.createElement('div');
43 | containerElt.className = 'hidden-rendering-container';
44 | document.body.appendChild(containerElt);
45 | 
46 | let init = () => {
47 |   window.mermaid.initialize(config);
48 |   init = () => {};
49 | };
50 | 
51 | const render = (elt) => {
52 |   try {
53 |     init();
54 |     const svgId = `mermaid-svg-${utils.uid()}`;
55 |     window.mermaid.mermaidAPI.render(svgId, elt.textContent, () => {
56 |       while (elt.firstChild) {
57 |         elt.removeChild(elt.lastChild);
58 |       }
59 |       elt.appendChild(containerElt.querySelector(`#${svgId}`));
60 |     }, containerElt);
61 |   } catch (e) {
62 |     console.error(e); // eslint-disable-line no-console
63 |   }
64 | };
65 | 
66 | extensionSvc.onGetOptions((options, properties) => {
67 |   options.mermaid = properties.extensions.mermaid.enabled;
68 | });
69 | 
70 | extensionSvc.onSectionPreview((elt) => {
71 |   elt.querySelectorAll('.prism.language-mermaid')
72 |     .cl_each(diagramElt => render(diagramElt.parentNode));
73 | });
74 | 


--------------------------------------------------------------------------------
/src/icons/Alert.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 13,14L 11,14L 11,9.99998L 13,9.99998M 13,18L 11,18L 11,16L 13,16M 1,21L 23,21L 12,1.99998L 1,21 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/ArrowLeft.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 20,11L 20,13L 7.98958,13L 13.4948,18.5052L 12.0806,19.9194L 4.16116,12L 12.0806,4.08058L 13.4948,5.49479L 7.98958,11L 20,11 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/CheckCircle.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 12,2C 17.5228,2 22,6.47716 22,12C 22,17.5228 17.5228,22 12,22C 6.47715,22 2,17.5228 2,12C 2,6.47716 6.47715,2 12,2 Z M 10.9999,16.5019L 17.9999,9.50193L 16.5859,8.08794L 10.9999,13.6739L 7.91391,10.5879L 6.49991,12.0019L 10.9999,16.5019 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Close.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/CodeBraces.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 8,3C 6.89543,3 6,3.89539 6,5L 6,9C 6,10.1046 5.10457,11 4,11L 3,11L 3,13L 4,13C 5.10457,13 6,13.8954 6,15L 6,19C 6,20.1046 6.92841,20.7321 8,21L 10,21L 10,19L 8,19L 8,14C 8,12.8954 7.10457,12 6,12C 7.10457,12 8,11.1046 8,10L 8,5L 10,5L 10,3M 16,3C 17.1046,3 18,3.89539 18,5L 18,9C 18,10.1046 18.8954,11 20,11L 21,11L 21,13L 20,13C 18.8954,13 18,13.8954 18,15L 18,19C 18,20.1046 17.0716,20.7321 16,21L 14,21L 14,19L 16,19L 16,14C 16,12.8954 16.8954,12 18,12C 16.8954,12 16,11.1046 16,10L 16,5L 14,5L 14,3L 16,3 Z " />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/CodeTags.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="2 2 20 20">
3 |     <path d="M 14.6,16.6L 19.2,12L 14.6,7.4L 16,6L 22,12L 16,18L 14.6,16.6 Z M 9.4,16.6L 4.8,12L 9.4,7.4L 8,6L 2,12L 8,18L 9.4,16.6 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/ContentCopy.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 19,21L 8,21L 8,7L 19,7M 19,5L 8,5C 6.9,5 6,5.9 6,7L 6,21C 6,22.1 6.9,23 8,23L 19,23C 20.1,23 21,22.1 21,21L 21,7C 21,5.9 20.1,5 19,5 Z M 16,1L 4,1C 2.9,1 2,1.9 2,3L 2,17L 4,17L 4,3L 16,3L 16,1 Z " />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/ContentSave.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M15,9H5V5H15M12,19C10.34,19 9,17.66 9,16C9,14.34 10.34,13 12,13C13.66,13 15,14.34 15,16C15,17.66 13.66,19 12,19M17,3H5C3.89,3 3,3.9 3,5V19C3,20.1 3.9,21 5,21H19C20.1,21 21,20.1 21,19V7L17,3Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Database.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M12,3C7.58,3 4,4.79 4,7C4,9.21 7.58,11 12,11C16.42,11 20,9.21 20,7C20,4.79 16.42,3 12,3M4,9V12C4,14.21 7.58,16 12,16C16.42,16 20,14.21 20,12V9C20,11.21 16.42,13 12,13C7.58,13 4,11.21 4,9M4,14V17C4,19.21 7.58,21 12,21C16.42,21 20,19.21 20,17V14C20,16.21 16.42,18 12,18C7.58,18 4,16.21 4,14Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Delete.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19C6,20.1 6.9,21 8,21H16C17.1,21 18,20.1 18,19V7H6V19Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/DotsHorizontal.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 16,12C 16,10.8954 16.8954,10 18,10C 19.1046,10 20,10.8954 20,12C 20,13.1046 19.1046,14 18,14C 16.8954,14 16,13.1046 16,12 Z M 10,12C 10,10.8954 10.8954,10 12,10C 13.1046,10 14,10.8954 14,12C 14,13.1046 13.1046,14 12,14C 10.8954,14 10,13.1046 10,12 Z M 4,12C 4,10.8954 4.89543,10 6,10C 7.10457,10 8,10.8954 8,12C 8,13.1046 7.10457,14 6,14C 4.89543,14 4,13.1046 4,12 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Download.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 4.9994,19.9981L 18.9994,19.9981L 18.9994,17.9981L 4.9994,17.9981M 18.9994,8.99807L 14.9994,8.99807L 14.9994,2.99807L 8.9994,2.99807L 8.9994,8.99807L 4.9994,8.99807L 11.9994,15.9981L 18.9994,8.99807 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Eye.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 11.9994,8.99813C 10.3424,8.99813 8.99941,10.3411 8.99941,11.9981C 8.99941,13.6551 10.3424,14.9981 11.9994,14.9981C 13.6564,14.9981 14.9994,13.6551 14.9994,11.9981C 14.9994,10.3411 13.6564,8.99813 11.9994,8.99813 Z M 11.9994,16.9981C 9.23841,16.9981 6.99941,14.7591 6.99941,11.9981C 6.99941,9.23714 9.23841,6.99813 11.9994,6.99813C 14.7604,6.99813 16.9994,9.23714 16.9994,11.9981C 16.9994,14.7591 14.7604,16.9981 11.9994,16.9981 Z M 11.9994,4.49813C 6.99741,4.49813 2.72741,7.60915 0.99941,11.9981C 2.72741,16.3871 6.99741,19.4981 11.9994,19.4981C 17.0024,19.4981 21.2714,16.3871 22.9994,11.9981C 21.2714,7.60915 17.0024,4.49813 11.9994,4.49813 Z " />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/FileImage.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 12.9994,8.99807L 18.4994,8.99807L 12.9994,3.49807L 12.9994,8.99807 Z M 5.99938,1.99809L 13.9994,1.99809L 19.9994,7.99808L 19.9994,19.9981C 19.9994,21.1021 19.1034,21.9981 17.9994,21.9981L 5.98937,21.9981C 4.88537,21.9981 3.99939,21.1021 3.99939,19.9981L 4.0094,3.99808C 4.0094,2.89407 4.89437,1.99809 5.99938,1.99809 Z M 6,20L 15,20L 18,20L 18,12L 14,16L 12,14L 6,20 Z M 8,9C 6.89543,9 6,9.89543 6,11C 6,12.1046 6.89543,13 8,13C 9.10457,13 10,12.1046 10,11C 10,9.89543 9.10457,9 8,9 Z " />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/FileMultiple.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="-2 -2 26 26">
3 |     <path d="M15,7H20.5L15,1.5V7M8,0H16L22,6V18C22,19.1 21.1,20 20,20H8C6.89,20 6,19.1 6,18V2C6,0.9 6.9,0 8,0M4,4V22H20V24H4C2.9,24 2,23.1 2,22V4H4Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/FilePlus.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M13,9H18.5L13,3.5V9M6,2H14L20,8V20C20,21.1 19.1,22 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M11,15V12H9V15H6V17H9V20H11V17H14V15H11Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Folder.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M10,4H4C2.89,4 2,4.89 2,6V18C2,19.1 2.9,20 4,20H20C21.1,20 22,19.1 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/FolderMultiple.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M22,4H14L12,2H6C4.9,2 4,2.9 4,4V16C4,17.1 4.9,18 6,18H22C23.1,18 24,17.1 24,16V6C24,4.9 23.1,4 22,4M2,6H0V11H0V20C0,21.1 0.9,22 2,22H20V20H2V6Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/FolderPlus.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M10,4L12,6H20C21.1,6 22,6.9 22,8V18C22,19.1 21.1,20 20,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4H10M15,9V12H12V14H15V17H17V14H20V12H17V9H15Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/FormatBold.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M13.35,17.401l-4.201,0l0,-3.601l4.201,0c0.997,0 1.801,0.805 1.801,1.801c0,0.996 -0.804,1.8 -1.801,1.8m-4.201,-10.802l3.601,0c0.996,0 1.801,0.804 1.801,1.8c0,0.996 -0.805,1.801 -1.801,1.801l-3.601,0m6.722,1.548c1.164,-0.816 1.98,-2.149 1.98,-3.349c0,-2.712 -2.1,-4.801 -4.801,-4.801l-7.502,0l0,16.804l8.45,0c2.521,0 4.454,-2.04 4.454,-4.549c0,-1.825 -1.033,-3.385 -2.581,-4.105Z" style="fill-rule:nonzero;"/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/FormatItalic.vue:
--------------------------------------------------------------------------------
1 | <template>
2 | 	<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 | 		<path d="M8.617,3.658l0,3.575l2.633,0l-2.075,9.534l-3.325,0l0,3.575l9.533,0l0,-3.575l-2.633,0l2.075,-9.534l3.325,0l0,-3.575l-9.533,0Z" style="fill-rule:nonzero;"/>
4 | 	</svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/FormatListBulleted.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M7.043,4.695l14.61,0l0,2.087l-14.61,0l0,-2.087m0,8.349l0,-2.088l14.61,0l0,2.088l-14.61,0m-3.131,-8.871c0.866,0 1.566,0.699 1.566,1.565c0,0.867 -0.7,1.566 -1.566,1.566c-0.866,0 -1.565,-0.699 -1.565,-1.566c0,-0.866 0.699,-1.565 1.565,-1.565m0,6.262c0.866,0 1.566,0.699 1.566,1.565c0,0.866 -0.7,1.565 -1.566,1.565c-0.866,0 -1.565,-0.699 -1.565,-1.565c0,-0.866 0.699,-1.565 1.565,-1.565m3.131,8.87l0,-2.087l14.61,0l0,2.087l-14.61,0m-3.131,-2.609c0.866,0 1.566,0.699 1.566,1.566c0,0.866 -0.7,1.565 -1.566,1.565c-0.866,0 -1.565,-0.699 -1.565,-1.565c0,-0.867 0.699,-1.566 1.565,-1.566Z" style="fill-rule:nonzero;"/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/FormatListChecks.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M3,5H9V11H3V5M5,7V9H7V7H5M11,7H21V9H11V7M11,15H21V17H11V15M5,20L1.5,16.5L2.91,15.09L5,17.17L9.59,12.59L11,14L5,20Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/FormatListNumbers.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M7.235,13.059l14.825,0l0,-2.118l-14.825,0m0,8.471l14.825,0l0,-2.117l-14.825,0m0,-10.59l14.825,0l0,-2.117l-14.825,0m-5.295,6.353l1.906,0l-1.906,2.224l0,0.953l3.177,0l0,-1.059l-1.906,0l1.906,-2.224l0,-0.953l-3.177,0m1.059,-2.118l1.059,0l0,-4.235l-2.118,0l0,1.059l1.059,0m-1.059,12.707l2.118,0l0,0.529l-1.059,0l0,1.059l1.059,0l0,0.529l-2.118,0l0,1.059l3.177,0l0,-4.235l-3.177,0l0,1.059Z" style="fill-rule:nonzero;"/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/FormatQuoteClose.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M14.446,18.235l2.92,0l1.946,-4.988l0,-7.482l-5.839,0l0,7.482l2.92,0m-10.732,4.988l2.919,0l1.947,-4.988l0,-7.482l-5.839,0l0,7.482l2.919,0l-1.946,4.988Z" style="fill-rule:nonzero;"/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/FormatSize.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M2.007,12.526l3.156,0l0,7.363l3.155,0l0,-7.363l3.156,0l0,-3.156l-9.467,0m6.311,-5.259l0,3.155l5.26,0l0,12.623l3.156,0l0,-12.623l5.259,0l0,-3.155l-13.675,0Z" style="fill-rule:nonzero;"/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/FormatStrikethrough.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M20.874,12.059l0,1.729l-3.541,0c0.806,1.851 0.766,6.918 -5.026,6.918c-6.721,0.043 -6.463,-5.621 -6.463,-5.621l3.203,0.044c0.024,2.914 2.55,2.914 3.05,2.879c0.516,-0.043 2.444,-0.034 2.598,-2.058c0.064,-0.942 -0.823,-1.66 -1.791,-2.162l-9.778,0l0,-1.729l17.748,0m-2.896,-3.554l-3.211,-0.026c0,0 0.137,-2.395 -2.646,-2.404c-2.783,-0.017 -2.541,1.902 -2.541,2.144c0.032,0.243 0.274,1.436 2.42,2.007l-5.074,0c0,0 -2.816,-5.82 4.057,-6.814c7.027,-1.038 7.011,5.11 6.995,5.093Z" style="fill-rule:nonzero;"/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/HelpCircle.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 15.0661,11.2518L 14.1711,12.1697C 13.4471,12.8937 12.9991,13.4977 12.9991,14.9977L 10.9991,14.9977L 10.9991,14.4977C 10.9991,13.3937 11.4471,12.3937 12.1711,11.6697L 13.4141,10.4117C 13.7751,10.0497 13.9991,9.54974 13.9991,8.99774C 13.9991,7.89374 13.1041,6.99774 11.9991,6.99774C 10.8951,6.99774 9.99908,7.89374 9.99908,8.99774L 7.99908,8.99774C 7.99908,6.78876 9.7901,4.99774 11.9991,4.99774C 14.2091,4.99774 15.9991,6.78876 15.9991,8.99774C 15.9991,9.87775 15.6431,10.6747 15.0661,11.2518 Z M 12.9991,18.9977L 10.9991,18.9977L 10.9991,16.9977L 12.9991,16.9977M 11.9991,1.99774C 6.4761,1.99774 1.99908,6.47473 1.99908,11.9977C 1.99908,17.5217 6.4761,21.9977 11.9991,21.9977C 17.5231,21.9977 21.9991,17.5217 21.9991,11.9977C 21.9991,6.47473 17.5231,1.99774 11.9991,1.99774 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/History.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M11,7V12.11L15.71,14.9L16.5,13.62L12.5,11.25V7M12.5,2C8.97,2 5.91,3.92 4.27,6.77L2,4.5V11H8.5L5.75,8.25C6.96,5.73 9.5,4 12.5,4C16.64,4 20,7.36 20,11.5C20,15.64 16.64,19 12.5,19C9.23,19 6.47,16.91 5.44,14H3.34C4.44,18.03 8.11,21 12.5,21C17.74,21 22,16.75 22,11.5C22,6.25 17.75,2 12.5,2Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Information.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 12.9994,8.99805L 10.9994,8.99805L 10.9994,6.99805L 12.9994,6.99805M 12.9994,16.998L 10.9994,16.998L 10.9994,10.998L 12.9994,10.998M 11.9994,1.99805C 6.47642,1.99805 1.99943,6.47504 1.99943,11.998C 1.99943,17.5211 6.47642,21.998 11.9994,21.998C 17.5224,21.998 21.9994,17.5211 21.9994,11.998C 21.9994,6.47504 17.5224,1.99805 11.9994,1.99805 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Key.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 7,14C 5.9,14 5,13.1 5,12C 5,10.9 5.9,10 7,10C 8.1,10 9,10.9 9,12C 9,13.1 8.1,14 7,14 Z M 12.65,10C 11.83,7.67 9.61,6 7,6C 3.69,6 1,8.69 1,12C 1,15.31 3.69,18 7,18C 9.61,18 11.83,16.33 12.65,14L 17,14L 17,18L 21,18L 21,14L 23,14L 23,10L 12.65,10 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/LinkVariant.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 10.5858,13.4142C 10.9763,13.8047 10.9763,14.4379 10.5858,14.8284C 10.1952,15.2189 9.56207,15.2189 9.17154,14.8284C 7.21892,12.8758 7.21892,9.70995 9.17154,7.75733L 9.17157,7.75736L 12.707,4.2219C 14.6596,2.26928 17.8255,2.26929 19.7781,4.2219C 21.7307,6.17452 21.7307,9.34034 19.7781,11.293L 18.2925,12.7785C 18.3008,11.9583 18.1659,11.1368 17.8876,10.355L 18.3639,9.87865C 19.5355,8.70708 19.5355,6.80759 18.3639,5.63602C 17.1923,4.46445 15.2929,4.46445 14.1213,5.63602L 10.5858,9.17155C 9.41419,10.3431 9.41419,12.2426 10.5858,13.4142 Z M 13.4142,9.17155C 13.8047,8.78103 14.4379,8.78103 14.8284,9.17155C 16.781,11.1242 16.781,14.29 14.8284,16.2426L 14.8284,16.2426L 11.2929,19.7782C 9.34026,21.7308 6.17444,21.7308 4.22182,19.7782C 2.26921,17.8255 2.2692,14.6597 4.22182,12.7071L 5.70744,11.2215C 5.69913,12.0417 5.8341,12.8631 6.11234,13.645L 5.63601,14.1213C 4.46444,15.2929 4.46444,17.1924 5.63601,18.3639C 6.80758,19.5355 8.70708,19.5355 9.87865,18.3639L 13.4142,14.8284C 14.5858,13.6568 14.5858,11.7573 13.4142,10.5858C 13.0237,10.1952 13.0237,9.56207 13.4142,9.17155 Z " />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Login.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 10,17.25L 10,14L 3.00002,14L 3.00002,10L 10,10L 10,6.75L 15.25,12L 10,17.25 Z M 7.99999,2.00003L 17,2.00005C 18.1045,2.00005 19,2.89546 19,4.00003L 19,20C 19,21.1046 18.1045,22 17,22L 7.99999,22C 6.89542,22 5.99999,21.1046 5.99999,20L 6,16L 7.99999,16L 7.99999,20L 17,20L 17,4.00003L 7.99999,4.00002L 7.99999,8.00001L 6,8.00001L 5.99999,4.00002C 5.99999,2.89545 6.89542,2.00003 7.99999,2.00003 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Logout.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 16.9999,17.25L 16.9999,14L 9.99998,14L 9.99998,10L 16.9999,10L 16.9999,6.75L 22.2499,12L 16.9999,17.25 Z M 13,2.00002C 14.1046,2.00002 15,2.89545 15,4.00002L 15,8L 13,8L 13,4.00002L 4,4.00004L 4,20L 13,20L 13,16L 15,16L 15,20C 15,21.1046 14.1046,22 13,22L 4,22C 2.89543,22 2,21.1046 2,20L 2,4.00004C 2,2.89547 2.89543,2.00006 4,2.00006L 13,2.00002 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Magnify.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 9.5,3C 13.0899,3 16,5.91015 16,9.5C 16,11.1149 15.411,12.5923 14.4362,13.7291L 14.7071,14L 15.5,14L 20.5,19L 19,20.5L 14,15.5L 14,14.7071L 13.7291,14.4362C 12.5923,15.411 11.1149,16 9.5,16C 5.91015,16 3,13.0899 3,9.5C 3,5.91015 5.91015,3 9.5,3 Z M 9.5,5.00001C 7.01472,5.00001 5,7.01473 5,9.50001C 5,11.9853 7.01472,14 9.5,14C 11.9853,14 14,11.9853 14,9.50001C 14,7.01473 11.9853,5.00001 9.5,5.00001 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Menu.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 3,6L 21,6L 21,8L 3,8L 3,6 Z M 3,11L 21,11L 21,13L 3,13L 3,11 Z M 3,16L 21,16L 21,18L 3,18L 3,16 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Message.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 21.9891,3.99805C 21.9891,2.89404 21.1031,1.99805 19.9991,1.99805L 3.99913,1.99805C 2.89512,1.99805 1.99913,2.89404 1.99913,3.99805L 1.99913,15.998C 1.99913,17.1021 2.89512,17.998 3.99913,17.998L 17.9991,17.998L 21.9991,21.998L 21.9891,3.99805 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/NavigationBar.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M19,8.977l-14,0l0,10l14,0m0,2l-14,0c-1.104,0 -2,-0.896 -2,-2l0,-10c0,-1.105 0.896,-2 2,-2l14,0c1.105,0 2,0.895 2,2l0,10c0,1.104 -0.895,2 -2,2Z" />
4 |     <rect x="3" y="3.023" width="18" height="2" />
5 |   </svg>
6 | </template>
7 | 


--------------------------------------------------------------------------------
/src/icons/OpenInNew.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 14,3L 14,5L 17.59,5L 7.76,14.83L 9.17,16.24L 19,6.41L 19,10L 21,10L 21,3M 19,19L 5,19L 5,5L 12,5L 12,3L 5,3C 3.89,3 3,3.9 3,5L 3,19C 3,20.1 3.89,21 5,21L 19,21C 20.1,21 21,20.1 21,19L 21,12L 19,12L 19,19 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Pen.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 16.8363,2.73375C 16.45,2.73375 16.0688,2.88125 15.7712,3.17375L 13.6525,5.2925L 18.955,10.5962L 21.0737,8.47625C 21.665,7.89 21.665,6.94375 21.0737,6.3575L 17.895,3.17375C 17.6025,2.88125 17.2163,2.73375 16.8363,2.73375 Z M 12.9437,6.00125L 4.84375,14.1062L 7.4025,14.39L 7.57875,16.675L 9.85875,16.85L 10.1462,19.4088L 18.2475,11.3038M 4.2475,15.0437L 2.515,21.7337L 9.19875,19.9412L 8.955,17.7838L 6.645,17.6075L 6.465,15.2925"/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Printer.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M18,3H6V7H18M19,12C18.45,12 18,11.55 18,11C18,10.45 18.45,10 19,10C19.55,10 20,10.45 20,11C20,11.55 19.55,12 19,12M16,19H8V14H16M19,8H5C3.34,8 2,9.34 2,11V17H6V21H18V17H22V11C22,9.34 20.66,8 19,8Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Provider.vue:
--------------------------------------------------------------------------------
 1 | <template>
 2 |   <div class="icon-provider" :class="'icon-provider--' + classState">
 3 |     <icon-sync-off v-if="!classState"></icon-sync-off>
 4 |   </div>
 5 | </template>
 6 | 
 7 | <script>
 8 | export default {
 9 |   props: ['providerId'],
10 |   computed: {
11 |     classState() {
12 |       switch (this.providerId) {
13 |         case 'googleDrive':
14 |         case 'googleDriveAppData':
15 |         case 'googleDriveWorkspace':
16 |           return 'google-drive';
17 |         case 'googlePhotos':
18 |           return 'google-photos';
19 |         case 'githubWorkspace':
20 |           return 'github';
21 |         case 'gist':
22 |           return 'github';
23 |         case 'gitlabWorkspace':
24 |           return 'gitlab';
25 |         case 'bloggerPage':
26 |           return 'blogger';
27 |         case 'couchdbWorkspace':
28 |           return 'couchdb';
29 |         default:
30 |           return this.providerId;
31 |       }
32 |     },
33 |   },
34 | };
35 | </script>
36 | 
37 | <style lang="scss">
38 | .icon-provider {
39 |   width: 100%;
40 |   height: 100%;
41 |   background-position: center;
42 |   background-repeat: no-repeat;
43 |   background-size: contain;
44 | }
45 | 
46 | .icon-provider--stackedit {
47 |   background-image: url(../assets/iconStackedit.svg);
48 | }
49 | 
50 | .icon-provider--google-drive {
51 |   background-image: url(../assets/iconGoogleDrive.svg);
52 | }
53 | 
54 | .icon-provider--google-photos {
55 |   background-image: url(../assets/iconGooglePhotos.svg);
56 | }
57 | 
58 | .icon-provider--github {
59 |   background-image: url(../assets/iconGithub.svg);
60 | }
61 | 
62 | .icon-provider--gitlab {
63 |   background-image: url(../assets/iconGitlab.svg);
64 | }
65 | 
66 | .icon-provider--google {
67 |   background-image: url(../assets/iconGoogle.svg);
68 | }
69 | 
70 | .icon-provider--dropbox {
71 |   background-image: url(../assets/iconDropbox.svg);
72 | }
73 | 
74 | .icon-provider--wordpress {
75 |   background-image: url(../assets/iconWordpress.svg);
76 | }
77 | 
78 | .icon-provider--blogger {
79 |   background-image: url(../assets/iconBlogger.svg);
80 | }
81 | 
82 | .icon-provider--zendesk {
83 |   background-image: url(../assets/iconZendesk.svg);
84 | }
85 | 
86 | .icon-provider--couchdb {
87 |   background-image: url(../assets/iconCouchdb.svg);
88 | }
89 | </style>
90 | 


--------------------------------------------------------------------------------
/src/icons/Redo.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M18.4,10.6C16.55,9 14.15,8 11.5,8C6.85,8 2.92,11.03 1.54,15.22L3.9,16C4.95,12.81 7.95,10.5 11.5,10.5C13.45,10.5 15.23,11.22 16.62,12.38L13,16H22V7L18.4,10.6Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/ScrollSync.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M9,18l3,0l-4,4l-4,-4l3,0l0,-3l2,0l0,3Zm8,0l3,0l-4,4l-4,-4l3,0l0,-3l2,0l0,3Zm0.055,-5l-10.11,0l0,-2l10.11,0l0,2Zm-8.055,-4l-2,0l0,-3l-3,0l4,-4l4,4l4,-4l4,4l-3,0l0,3l-2,0l0,-3l-6,0l0,3Z"/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Seal.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="1 1 23 23">
3 |     <path d="M 20.3943,19.3706L 16.3828,17.9893L 15.0016,22.0008L 11.9248,15.9996L 8.99895,21.9986L 7.61768,17.9871L 3.60619,19.3683L 6.53159,13.3704C 5.57315,12.1727 5,10.6533 5,9C 5,5.13401 8.13401,2 12,2C 15.866,2 19,5.13401 19,9C 19,10.6535 18.4267,12.1731 17.468,13.3708L 20.3943,19.3706 Z M 7,9.00001L 9.68578,10.3429L 9.50615,13.3356L 12.012,11.6811L 14.514,13.333L 14.334,10.3356L 17.0156,8.9948L 14.323,7.64851L 14.5017,4.6727L 12.0162,6.31371L 9.49384,4.64828L 9.67477,7.66262L 7,9.00001 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Settings.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 11.9994,15.498C 10.0664,15.498 8.49939,13.931 8.49939,11.998C 8.49939,10.0651 10.0664,8.49805 11.9994,8.49805C 13.9324,8.49805 15.4994,10.0651 15.4994,11.998C 15.4994,13.931 13.9324,15.498 11.9994,15.498 Z M 19.4284,12.9741C 19.4704,12.6531 19.4984,12.329 19.4984,11.998C 19.4984,11.6671 19.4704,11.343 19.4284,11.022L 21.5414,9.36804C 21.7294,9.21606 21.7844,8.94604 21.6594,8.73004L 19.6594,5.26605C 19.5354,5.05005 19.2734,4.96204 19.0474,5.04907L 16.5584,6.05206C 16.0424,5.65607 15.4774,5.32104 14.8684,5.06903L 14.4934,2.41907C 14.4554,2.18103 14.2484,1.99805 13.9994,1.99805L 9.99939,1.99805C 9.74939,1.99805 9.5434,2.18103 9.5054,2.41907L 9.1304,5.06805C 8.52039,5.32104 7.95538,5.65607 7.43939,6.05206L 4.95139,5.04907C 4.7254,4.96204 4.46338,5.05005 4.33939,5.26605L 2.33939,8.73004C 2.21439,8.94604 2.26938,9.21606 2.4574,9.36804L 4.5694,11.022C 4.5274,11.342 4.49939,11.6671 4.49939,11.998C 4.49939,12.329 4.5274,12.6541 4.5694,12.9741L 2.4574,14.6271C 2.26938,14.78 2.21439,15.05 2.33939,15.2661L 4.33939,18.73C 4.46338,18.946 4.7254,19.0341 4.95139,18.947L 7.4404,17.944C 7.95639,18.34 8.52139,18.675 9.1304,18.9271L 9.5054,21.577C 9.5434,21.8151 9.74939,21.998 9.99939,21.998L 13.9994,21.998C 14.2484,21.998 14.4554,21.8151 14.4934,21.577L 14.8684,18.9271C 15.4764,18.6741 16.0414,18.34 16.5574,17.9431L 19.0474,18.947C 19.2734,19.0341 19.5354,18.946 19.6594,18.73L 21.6594,15.2661C 21.7844,15.05 21.7294,14.78 21.5414,14.6271L 19.4284,12.9741 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/SidePreview.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M11,20.977l-6,0c-1.104,0 -2,-0.896 -2,-2l0,-14c0,-1.105 0.896,-2 2,-2l14,0c1.105,0 2,0.895 2,2l0,14c0,1.104 -0.895,2 -2,2l-6,0l0,0.023l-2,0l0,-0.023Zm0,-2l0,-14l-6,0l0,14l6,0Zm8,-14l-6,0l0,14l6,0l0,-14Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/SignalOff.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 18,3L 18,16.1777L 21,19.1777L 21,3L 18,3 Z M 4.27734,5L 3,6.2676L 10.7324,14L 8,14L 8,21L 11,21L 11,14.2676L 13,16.2676L 13,21L 16,21L 16,19.2676L 19.7324,23L 21,21.7227L 4.27734,5 Z M 13,9L 13,11.1777L 16,14.1777L 16,9L 13,9 Z M 3,18L 3,21L 6,21L 6,18L 3,18 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/StatusBar.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M19,15.023l-14,0l0,-10l14,0m0,-2l-14,0c-1.104,0 -2,0.896 -2,2l0,10c0,1.105 0.896,2 2,2l14,0c1.105,0 2,-0.895 2,-2l0,-10c0,-1.104 -0.895,-2 -2,-2Z" />
4 |     <rect x="3" y="18.977" width="18" height="2" />
5 |   </svg>
6 | </template>
7 | 


--------------------------------------------------------------------------------
/src/icons/Sync.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M12,18C8.69,18 6,15.31 6,12C6,11 6.25,10.03 6.7,9.2L5.24,7.74C4.46,8.97 4,10.43 4,12C4,16.42 7.58,20 12,20V23L16,19L12,15M12,4V1L8,5L12,9V6C15.31,6 18,8.69 18,12C18,13 17.75,13.97 17.3,14.8L18.76,16.26C19.54,15.03 20,13.57 20,12C20,7.58 16.42,4 12,4Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/SyncOff.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M20,4H14V10L16.24,7.76C17.32,8.85 18,10.34 18,12C18,13 17.75,13.94 17.32,14.77L18.78,16.23C19.55,15 20,13.56 20,12C20,9.79 19.09,7.8 17.64,6.36L20,4M2.86,5.41L5.22,7.77C4.45,9 4,10.44 4,12C4,14.21 4.91,16.2 6.36,17.64L4,20H10V14L7.76,16.24C6.68,15.15 6,13.66 6,12C6,11 6.25,10.06 6.68,9.23L14.76,17.31C14.5,17.44 14.26,17.56 14,17.65V19.74C14.79,19.53 15.54,19.2 16.22,18.78L18.58,21.14L19.85,19.87L4.14,4.14L2.86,5.41M10,6.35V4.26C9.2,4.47 8.45,4.8 7.77,5.22L9.23,6.68C9.5,6.56 9.73,6.44 10,6.35Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Table.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 5,4L 19,4C 20.1046,4 21,4.89543 21,6L 21,18C 21,19.1046 20.1046,20 19,20L 5,20C 3.89543,20 3,19.1046 3,18L 3,6C 3,4.89543 3.89543,4 5,4 Z M 5,8L 5,12L 11,12L 11,8L 5,8 Z M 13,8L 13,12L 19,12L 19,8L 13,8 Z M 5,14L 5,18L 11,18L 11,14L 5,14 Z M 13,14L 13,18L 19,18L 19,14L 13,14 Z " />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Target.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 11.0013,2.0025L 11.0013,4.0725C 7.3825,4.53125 4.53125,7.3825 4.0725,11.0012L 2.0025,11.0012L 2.0025,12.9975L 4.0725,12.9975C 4.53125,16.6213 7.3825,19.4675 11.0013,19.9262L 11.0013,22.0025L 12.9975,22.0025L 12.9975,19.9313C 16.6212,19.4675 19.4675,16.6213 19.9263,12.9975L 22.0025,12.9975L 22.0025,11.0012L 19.9312,11.0012C 19.4675,7.3825 16.6212,4.53125 12.9975,4.0725L 12.9975,2.0025M 11.0013,6.08375L 11.0013,7.9975L 12.9975,7.9975L 12.9975,6.08875C 15.5175,6.51375 17.485,8.48625 17.915,11.0012L 16.0012,11.0012L 16.0012,12.9975L 17.91,12.9975C 17.485,15.5175 15.5125,17.485 12.9975,17.915L 12.9975,16.0012L 11.0013,16.0012L 11.0013,17.91C 8.48625,17.485 6.51375,15.5125 6.08375,12.9975L 7.9975,12.9975L 7.9975,11.0012L 6.08875,11.0012C 6.51375,8.48625 8.48625,6.51375 11.0013,6.08375 Z M 12.0025,11.0012C 11.445,11.0012 11.0013,11.445 11.0013,12.0025C 11.0013,12.5538 11.445,12.9975 12.0025,12.9975C 12.5537,12.9975 12.9975,12.5538 12.9975,12.0025C 12.9975,11.445 12.5537,11.0012 12.0025,11.0012 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Toc.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M3 9h14V7H3v2zm0 4h14v-2H3v2zm0 4h14v-2H3v2zm16 0h2v-2h-2v2zm0-10v2h2V7h-2zm0 6h2v-2h-2v2z"/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Undo.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M12.5,8C9.85,8 7.45,9 5.6,10.6L2,7V16H11L7.38,12.38C8.77,11.22 10.54,10.5 12.5,10.5C16.04,10.5 19.05,12.81 20.1,16L22.47,15.22C21.08,11.03 17.15,8 12.5,8Z" />
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/Upload.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 8.99939,15.998L 8.99939,9.99805L 4.99939,9.99805L 11.9994,2.99805L 18.9994,9.99805L 14.9994,9.99805L 14.9994,15.998L 8.99939,15.998 Z M 4.99937,19.9981L 4.99937,17.9981L 18.9994,17.9981L 18.9994,19.9981L 4.99937,19.9981 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/icons/ViewList.vue:
--------------------------------------------------------------------------------
1 | <template>
2 |   <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
3 |     <path d="M 9,5L 9,9L 21,9L 21,5M 9,19L 21,19L 21,15L 9,15M 9,14L 21,14L 21,10L 9,10M 4,9L 8,9L 8,5L 4,5M 4,19L 8,19L 8,15L 4,15M 4,14L 8,14L 8,10L 4,10L 4,14 Z "/>
4 |   </svg>
5 | </template>
6 | 


--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue';
 2 | import 'babel-polyfill';
 3 | import 'indexeddbshim/dist/indexeddbshim';
 4 | import * as OfflinePluginRuntime from 'offline-plugin/runtime';
 5 | import './extensions';
 6 | import './services/optional';
 7 | import './icons';
 8 | import App from './components/App';
 9 | import store from './store';
10 | import localDbSvc from './services/localDbSvc';
11 | 
12 | if (!indexedDB) {
13 |   throw new Error('Your browser is not supported. Please upgrade to the latest version.');
14 | }
15 | 
16 | OfflinePluginRuntime.install({
17 |   onUpdateReady: () => {
18 |     // Tells to new SW to take control immediately
19 |     OfflinePluginRuntime.applyUpdate();
20 |   },
21 |   onUpdated: async () => {
22 |     if (!store.state.light) {
23 |       await localDbSvc.sync();
24 |       localStorage.updated = true;
25 |       // Reload the webpage to load into the new version
26 |       window.location.reload();
27 |     }
28 |   },
29 | });
30 | 
31 | if (localStorage.updated) {
32 |   store.dispatch('notification/info', 'StackEdit has just updated itself!');
33 |   setTimeout(() => localStorage.removeItem('updated'), 2000);
34 | }
35 | 
36 | if (!localStorage.installPrompted) {
37 |   window.addEventListener('beforeinstallprompt', async (promptEvent) => {
38 |     // Prevent Chrome 67 and earlier from automatically showing the prompt
39 |     promptEvent.preventDefault();
40 | 
41 |     try {
42 |       await store.dispatch('notification/confirm', 'Add StackEdit to your home screen?');
43 |       promptEvent.prompt();
44 |       await promptEvent.userChoice;
45 |     } catch (err) {
46 |       // Cancel
47 |     }
48 |     localStorage.installPrompted = true;
49 |   });
50 | }
51 | 
52 | Vue.config.productionTip = false;
53 | 
54 | /* eslint-disable no-new */
55 | new Vue({
56 |   el: '#app',
57 |   store,
58 |   render: h => h(App),
59 | });
60 | 


--------------------------------------------------------------------------------
/src/services/backupSvc.js:
--------------------------------------------------------------------------------
 1 | import workspaceSvc from './workspaceSvc';
 2 | import utils from './utils';
 3 | 
 4 | export default {
 5 |   async importBackup(jsonValue) {
 6 |     const fileNameMap = {};
 7 |     const folderNameMap = {};
 8 |     const parentIdMap = {};
 9 |     const textMap = {};
10 |     const propertiesMap = {};
11 |     const discussionsMap = {};
12 |     const commentsMap = {};
13 |     const folderIdMap = {
14 |       trash: 'trash',
15 |     };
16 | 
17 |     // Parse JSON value
18 |     const parsedValue = JSON.parse(jsonValue);
19 |     Object.entries(parsedValue).forEach(([id, value]) => {
20 |       if (value) {
21 |         const v4Match = id.match(/^file\.([^.]+)\.([^.]+)$/);
22 |         if (v4Match) {
23 |           // StackEdit v4 format
24 |           const [, v4Id, type] = v4Match;
25 |           if (type === 'title') {
26 |             fileNameMap[v4Id] = value;
27 |           } else if (type === 'content') {
28 |             textMap[v4Id] = value;
29 |           }
30 |         } else if (value.type === 'folder') {
31 |           // StackEdit v5 folder
32 |           folderIdMap[id] = utils.uid();
33 |           folderNameMap[id] = value.name;
34 |           parentIdMap[id] = `${value.parentId || ''}`;
35 |         } else if (value.type === 'file') {
36 |           // StackEdit v5 file
37 |           fileNameMap[id] = value.name;
38 |           parentIdMap[id] = `${value.parentId || ''}`;
39 |         } else if (value.type === 'content') {
40 |           // StackEdit v5 content
41 |           const [fileId] = id.split('/');
42 |           if (fileId) {
43 |             textMap[fileId] = value.text;
44 |             propertiesMap[fileId] = value.properties;
45 |             discussionsMap[fileId] = value.discussions;
46 |             commentsMap[fileId] = value.comments;
47 |           }
48 |         }
49 |       }
50 |     });
51 | 
52 |     await utils.awaitSequence(
53 |       Object.keys(folderNameMap),
54 |       async externalId => workspaceSvc.setOrPatchItem({
55 |         id: folderIdMap[externalId],
56 |         type: 'folder',
57 |         name: folderNameMap[externalId],
58 |         parentId: folderIdMap[parentIdMap[externalId]],
59 |       }),
60 |     );
61 | 
62 |     await utils.awaitSequence(
63 |       Object.keys(fileNameMap),
64 |       async externalId => workspaceSvc.createFile({
65 |         name: fileNameMap[externalId],
66 |         parentId: folderIdMap[parentIdMap[externalId]],
67 |         text: textMap[externalId],
68 |         properties: propertiesMap[externalId],
69 |         discussions: discussionsMap[externalId],
70 |         comments: commentsMap[externalId],
71 |       }, true),
72 |     );
73 |   },
74 | };
75 | 


--------------------------------------------------------------------------------
/src/services/badgeSvc.js:
--------------------------------------------------------------------------------
 1 | import store from '../store';
 2 | 
 3 | let lastEarnedFeatureIds = null;
 4 | let debounceTimeoutId;
 5 | 
 6 | const showInfo = () => {
 7 |   const earnedBadges = store.getters['data/allBadges']
 8 |     .filter(badge => badge.isEarned && !lastEarnedFeatureIds.has(badge.featureId));
 9 |   if (earnedBadges.length) {
10 |     store.dispatch('notification/badge', earnedBadges.length > 1
11 |       ? `You've earned ${earnedBadges.length} badges: ${earnedBadges.map(badge => `"${badge.name}"`).join(', ')}.`
12 |       : `You've earned 1 badge: "${earnedBadges[0].name}".`);
13 |   }
14 |   lastEarnedFeatureIds = null;
15 | };
16 | 
17 | export default {
18 |   addBadge(featureId) {
19 |     if (!store.getters['data/badgeCreations'][featureId]) {
20 |       if (!lastEarnedFeatureIds) {
21 |         const earnedFeatureIds = store.getters['data/allBadges']
22 |           .filter(badge => badge.isEarned)
23 |           .map(badge => badge.featureId);
24 |         lastEarnedFeatureIds = new Set(earnedFeatureIds);
25 |       }
26 | 
27 |       store.dispatch('data/patchBadgeCreations', {
28 |         [featureId]: {
29 |           created: Date.now(),
30 |         },
31 |       });
32 | 
33 |       clearTimeout(debounceTimeoutId);
34 |       debounceTimeoutId = setTimeout(() => showInfo(), 5000);
35 |     }
36 |   },
37 | };
38 | 


--------------------------------------------------------------------------------
/src/services/editor/cledit/cleditMarker.js:
--------------------------------------------------------------------------------
 1 | import cledit from './cleditCore';
 2 | 
 3 | const DIFF_DELETE = -1;
 4 | const DIFF_INSERT = 1;
 5 | const DIFF_EQUAL = 0;
 6 | 
 7 | let idCounter = 0;
 8 | 
 9 | class Marker {
10 |   constructor(offset, trailing) {
11 |     this.id = idCounter;
12 |     idCounter += 1;
13 |     this.offset = offset;
14 |     this.trailing = trailing;
15 |   }
16 | 
17 |   adjustOffset(diffs) {
18 |     let startOffset = 0;
19 |     diffs.cl_each((diff) => {
20 |       const diffType = diff[0];
21 |       const diffText = diff[1];
22 |       const diffOffset = diffText.length;
23 |       switch (diffType) {
24 |         case DIFF_EQUAL:
25 |           startOffset += diffOffset;
26 |           break;
27 |         case DIFF_INSERT:
28 |           if (
29 |             this.trailing
30 |               ? this.offset > startOffset
31 |               : this.offset >= startOffset
32 |           ) {
33 |             this.offset += diffOffset;
34 |           }
35 |           startOffset += diffOffset;
36 |           break;
37 |         case DIFF_DELETE:
38 |           if (this.offset > startOffset) {
39 |             this.offset -= Math.min(diffOffset, this.offset - startOffset);
40 |           }
41 |           break;
42 |         default:
43 |       }
44 |     });
45 |   }
46 | }
47 | 
48 | 
49 | cledit.Marker = Marker;
50 | 


--------------------------------------------------------------------------------
/src/services/editor/cledit/cleditWatcher.js:
--------------------------------------------------------------------------------
 1 | import cledit from './cleditCore';
 2 | 
 3 | function Watcher(editor, listener) {
 4 |   this.isWatching = false;
 5 |   let contentObserver;
 6 |   this.startWatching = () => {
 7 |     this.stopWatching();
 8 |     this.isWatching = true;
 9 |     contentObserver = new window.MutationObserver(listener);
10 |     contentObserver.observe(editor.$contentElt, {
11 |       childList: true,
12 |       subtree: true,
13 |       characterData: true,
14 |     });
15 |   };
16 |   this.stopWatching = () => {
17 |     if (contentObserver) {
18 |       contentObserver.disconnect();
19 |       contentObserver = undefined;
20 |     }
21 |     this.isWatching = false;
22 |   };
23 |   this.noWatch = (cb) => {
24 |     if (this.isWatching === true) {
25 |       this.stopWatching();
26 |       cb();
27 |       this.startWatching();
28 |     } else {
29 |       cb();
30 |     }
31 |   };
32 | }
33 | 
34 | cledit.Watcher = Watcher;
35 | 


--------------------------------------------------------------------------------
/src/services/editor/cledit/index.js:
--------------------------------------------------------------------------------
 1 | import '../../../libs/clunderscore';
 2 | import cledit from './cleditCore';
 3 | import './cleditHighlighter';
 4 | import './cleditKeystroke';
 5 | import './cleditMarker';
 6 | import './cleditSelectionMgr';
 7 | import './cleditUndoMgr';
 8 | import './cleditUtils';
 9 | import './cleditWatcher';
10 | 
11 | export default cledit;
12 | 


--------------------------------------------------------------------------------
/src/services/extensionSvc.js:
--------------------------------------------------------------------------------
 1 | const getOptionsListeners = [];
 2 | const initConverterListeners = [];
 3 | const sectionPreviewListeners = [];
 4 | 
 5 | export default {
 6 |   onGetOptions(listener) {
 7 |     getOptionsListeners.push(listener);
 8 |   },
 9 | 
10 |   onInitConverter(priority, listener) {
11 |     initConverterListeners[priority] = listener;
12 |   },
13 | 
14 |   onSectionPreview(listener) {
15 |     sectionPreviewListeners.push(listener);
16 |   },
17 | 
18 |   getOptions(properties, isCurrentFile) {
19 |     return getOptionsListeners.reduce((options, listener) => {
20 |       listener(options, properties, isCurrentFile);
21 |       return options;
22 |     }, {});
23 |   },
24 | 
25 |   initConverter(markdown, options) {
26 |     // Use forEach as it's a sparsed array
27 |     initConverterListeners.forEach((listener) => {
28 |       listener(markdown, options);
29 |     });
30 |   },
31 | 
32 |   sectionPreview(elt, options, isEditor) {
33 |     sectionPreviewListeners.forEach((listener) => {
34 |       listener(elt, options, isEditor);
35 |     });
36 |   },
37 | };
38 | 


--------------------------------------------------------------------------------
/src/services/optional/index.js:
--------------------------------------------------------------------------------
1 | import './shortcuts';
2 | import './keystrokes';
3 | import './scrollSync';
4 | import './taskChange';
5 | 


--------------------------------------------------------------------------------
/src/services/optional/taskChange.js:
--------------------------------------------------------------------------------
 1 | import editorSvc from '../editorSvc';
 2 | import store from '../../store';
 3 | 
 4 | editorSvc.$on('inited', () => {
 5 |   const getPreviewOffset = (elt) => {
 6 |     let offset = 0;
 7 |     if (!elt || elt === editorSvc.previewElt) {
 8 |       return offset;
 9 |     }
10 |     let { previousSibling } = elt;
11 |     while (previousSibling) {
12 |       offset += previousSibling.textContent.length;
13 |       ({ previousSibling } = previousSibling);
14 |     }
15 |     return offset + getPreviewOffset(elt.parentNode);
16 |   };
17 | 
18 |   editorSvc.previewElt.addEventListener('click', (evt) => {
19 |     if (evt.target.classList.contains('task-list-item-checkbox')) {
20 |       evt.preventDefault();
21 |       if (store.getters['content/isCurrentEditable']) {
22 |         const editorContent = editorSvc.clEditor.getContent();
23 |         // Use setTimeout to ensure evt.target.checked has the old value
24 |         setTimeout(() => {
25 |           // Make sure content has not changed
26 |           if (editorContent === editorSvc.clEditor.getContent()) {
27 |             const previewOffset = getPreviewOffset(evt.target);
28 |             const endOffset = editorSvc.getEditorOffset(previewOffset + 1);
29 |             if (endOffset != null) {
30 |               const startOffset = editorContent.lastIndexOf('\n', endOffset) + 1;
31 |               const line = editorContent.slice(startOffset, endOffset);
32 |               const match = line.match(/^([ \t]*(?:[*+-]|\d+\.)[ \t]+\[)[ xX](\] .*)/);
33 |               if (match) {
34 |                 let newContent = editorContent.slice(0, startOffset);
35 |                 newContent += match[1];
36 |                 newContent += evt.target.checked ? ' ' : 'x';
37 |                 newContent += match[2];
38 |                 newContent += editorContent.slice(endOffset);
39 |                 editorSvc.clEditor.setContent(newContent, true);
40 |               }
41 |             }
42 |           }
43 |         }, 10);
44 |       }
45 |     }
46 |   });
47 | });
48 | 


--------------------------------------------------------------------------------
/src/services/providers/bloggerPageProvider.js:
--------------------------------------------------------------------------------
 1 | import store from '../../store';
 2 | import googleHelper from './helpers/googleHelper';
 3 | import Provider from './common/Provider';
 4 | 
 5 | export default new Provider({
 6 |   id: 'bloggerPage',
 7 |   name: 'Blogger Page',
 8 |   getToken({ sub }) {
 9 |     const token = store.getters['data/googleTokensBySub'][sub];
10 |     return token && token.isBlogger ? token : null;
11 |   },
12 |   getLocationUrl({ blogId, pageId }) {
13 |     return `https://www.blogger.com/blogger.g?blogID=${blogId}#editor/target=page;pageID=${pageId}`;
14 |   },
15 |   getLocationDescription({ pageId }) {
16 |     return pageId;
17 |   },
18 |   async publish(token, html, metadata, publishLocation) {
19 |     const page = await googleHelper.uploadBlogger({
20 |       token,
21 |       blogUrl: publishLocation.blogUrl,
22 |       blogId: publishLocation.blogId,
23 |       postId: publishLocation.pageId,
24 |       title: metadata.title,
25 |       content: html,
26 |       isPage: true,
27 |     });
28 |     return {
29 |       ...publishLocation,
30 |       blogId: page.blog.id,
31 |       pageId: page.id,
32 |     };
33 |   },
34 |   makeLocation(token, blogUrl, pageId) {
35 |     const location = {
36 |       providerId: this.id,
37 |       sub: token.sub,
38 |       blogUrl,
39 |     };
40 |     if (pageId) {
41 |       location.pageId = pageId;
42 |     }
43 |     return location;
44 |   },
45 | });
46 | 


--------------------------------------------------------------------------------
/src/services/providers/bloggerProvider.js:
--------------------------------------------------------------------------------
 1 | import store from '../../store';
 2 | import googleHelper from './helpers/googleHelper';
 3 | import Provider from './common/Provider';
 4 | 
 5 | export default new Provider({
 6 |   id: 'blogger',
 7 |   name: 'Blogger',
 8 |   getToken({ sub }) {
 9 |     const token = store.getters['data/googleTokensBySub'][sub];
10 |     return token && token.isBlogger ? token : null;
11 |   },
12 |   getLocationUrl({ blogId, postId }) {
13 |     return `https://www.blogger.com/blogger.g?blogID=${blogId}#editor/target=post;postID=${postId}`;
14 |   },
15 |   getLocationDescription({ postId }) {
16 |     return postId;
17 |   },
18 |   async publish(token, html, metadata, publishLocation) {
19 |     const post = await googleHelper.uploadBlogger({
20 |       ...publishLocation,
21 |       token,
22 |       title: metadata.title,
23 |       content: html,
24 |       labels: metadata.tags,
25 |       isDraft: metadata.status === 'draft',
26 |       published: metadata.date,
27 |     });
28 |     return {
29 |       ...publishLocation,
30 |       blogId: post.blog.id,
31 |       postId: post.id,
32 |     };
33 |   },
34 |   makeLocation(token, blogUrl, postId) {
35 |     const location = {
36 |       providerId: this.id,
37 |       sub: token.sub,
38 |       blogUrl,
39 |     };
40 |     if (postId) {
41 |       location.postId = postId;
42 |     }
43 |     return location;
44 |   },
45 | });
46 | 


--------------------------------------------------------------------------------
/src/services/providers/common/providerRegistry.js:
--------------------------------------------------------------------------------
1 | export default {
2 |   providersById: {},
3 |   register(provider) {
4 |     this.providersById[provider.id] = provider;
5 |     return provider;
6 |   },
7 | };
8 | 


--------------------------------------------------------------------------------
/src/services/providers/wordpressProvider.js:
--------------------------------------------------------------------------------
 1 | import store from '../../store';
 2 | import wordpressHelper from './helpers/wordpressHelper';
 3 | import Provider from './common/Provider';
 4 | 
 5 | export default new Provider({
 6 |   id: 'wordpress',
 7 |   name: 'WordPress',
 8 |   getToken({ sub }) {
 9 |     return store.getters['data/wordpressTokensBySub'][sub];
10 |   },
11 |   getLocationUrl({ siteId, postId }) {
12 |     return `https://wordpress.com/post/${siteId}/${postId}`;
13 |   },
14 |   getLocationDescription({ postId }) {
15 |     return postId;
16 |   },
17 |   async publish(token, html, metadata, publishLocation) {
18 |     const post = await wordpressHelper.uploadPost({
19 |       ...publishLocation,
20 |       ...metadata,
21 |       token,
22 |       content: html,
23 |     });
24 |     return {
25 |       ...publishLocation,
26 |       siteId: `${post.site_ID}`,
27 |       postId: `${post.ID}`,
28 |     };
29 |   },
30 |   makeLocation(token, domain, postId) {
31 |     const location = {
32 |       providerId: this.id,
33 |       sub: token.sub,
34 |       domain,
35 |     };
36 |     if (postId) {
37 |       location.postId = postId;
38 |     }
39 |     return location;
40 |   },
41 | });
42 | 


--------------------------------------------------------------------------------
/src/services/providers/zendeskProvider.js:
--------------------------------------------------------------------------------
 1 | import store from '../../store';
 2 | import zendeskHelper from './helpers/zendeskHelper';
 3 | import Provider from './common/Provider';
 4 | 
 5 | export default new Provider({
 6 |   id: 'zendesk',
 7 |   name: 'Zendesk',
 8 |   getToken({ sub }) {
 9 |     return store.getters['data/zendeskTokensBySub'][sub];
10 |   },
11 |   getLocationUrl({ sub, locale, articleId }) {
12 |     const token = this.getToken({ sub });
13 |     return `https://${token.subdomain}.zendesk.com/hc/${locale}/articles/${articleId}`;
14 |   },
15 |   getLocationDescription({ articleId }) {
16 |     return articleId;
17 |   },
18 |   async publish(token, html, metadata, publishLocation) {
19 |     const articleId = await zendeskHelper.uploadArticle({
20 |       ...publishLocation,
21 |       token,
22 |       title: metadata.title,
23 |       content: html,
24 |       labels: metadata.tags,
25 |       isDraft: metadata.status === 'draft',
26 |     });
27 |     return {
28 |       ...publishLocation,
29 |       articleId,
30 |     };
31 |   },
32 |   makeLocation(token, sectionId, locale, articleId) {
33 |     const location = {
34 |       providerId: this.id,
35 |       sub: token.sub,
36 |       sectionId,
37 |       locale,
38 |     };
39 |     if (articleId) {
40 |       location.articleId = articleId;
41 |     }
42 |     return location;
43 |   },
44 | });
45 | 


--------------------------------------------------------------------------------
/src/store/contentState.js:
--------------------------------------------------------------------------------
 1 | import moduleTemplate from './moduleTemplate';
 2 | import empty from '../data/empties/emptyContentState';
 3 | 
 4 | const module = moduleTemplate(empty, true);
 5 | 
 6 | module.getters = {
 7 |   ...module.getters,
 8 |   current: ({ itemsById }, getters, rootState, rootGetters) =>
 9 |     itemsById[`${rootGetters['file/current'].id}/contentState`] || empty(),
10 | };
11 | 
12 | module.actions = {
13 |   ...module.actions,
14 |   patchCurrent({ getters, commit }, value) {
15 |     commit('patchItem', {
16 |       ...value,
17 |       id: getters.current.id,
18 |     });
19 |   },
20 | };
21 | 
22 | export default module;
23 | 


--------------------------------------------------------------------------------
/src/store/contextMenu.js:
--------------------------------------------------------------------------------
 1 | const setter = propertyName => (state, value) => {
 2 |   state[propertyName] = value;
 3 | };
 4 | 
 5 | export default {
 6 |   namespaced: true,
 7 |   state: {
 8 |     coordinates: {
 9 |       left: 0,
10 |       top: 0,
11 |     },
12 |     items: [],
13 |     resolve: () => {},
14 |   },
15 |   mutations: {
16 |     setCoordinates: setter('coordinates'),
17 |     setItems: setter('items'),
18 |     setResolve: setter('resolve'),
19 |   },
20 |   actions: {
21 |     open({ commit, rootState }, { coordinates, items }) {
22 |       commit('setItems', items);
23 |       // Place the context menu outside the screen
24 |       commit('setCoordinates', { top: 0, left: -9999 });
25 |       // Let the UI refresh itself
26 |       setTimeout(() => {
27 |         // Take the size of the context menu and place it
28 |         const elt = document.querySelector('.context-menu__inner');
29 |         if (elt) {
30 |           const height = elt.offsetHeight;
31 |           if (coordinates.top + height > rootState.layout.bodyHeight) {
32 |             coordinates.top -= height;
33 |           }
34 |           if (coordinates.top < 0) {
35 |             coordinates.top = 0;
36 |           }
37 |           const width = elt.offsetWidth;
38 |           if (coordinates.left + width > rootState.layout.bodyWidth) {
39 |             coordinates.left -= width;
40 |           }
41 |           if (coordinates.left < 0) {
42 |             coordinates.left = 0;
43 |           }
44 |           commit('setCoordinates', coordinates);
45 |         }
46 |       }, 1);
47 | 
48 |       return new Promise(resolve => commit('setResolve', resolve));
49 |     },
50 |     close({ commit }) {
51 |       commit('setItems', []);
52 |       commit('setResolve', () => {});
53 |     },
54 |   },
55 | };
56 | 


--------------------------------------------------------------------------------
/src/store/file.js:
--------------------------------------------------------------------------------
 1 | import moduleTemplate from './moduleTemplate';
 2 | import empty from '../data/empties/emptyFile';
 3 | 
 4 | const module = moduleTemplate(empty);
 5 | 
 6 | module.state = {
 7 |   ...module.state,
 8 |   currentId: null,
 9 | };
10 | 
11 | module.getters = {
12 |   ...module.getters,
13 |   current: ({ itemsById, currentId }) => itemsById[currentId] || empty(),
14 |   isCurrentTemp: (state, { current }) => current.parentId === 'temp',
15 |   lastOpened: ({ itemsById }, { items }, rootState, rootGetters) =>
16 |     itemsById[rootGetters['data/lastOpenedIds'][0]] || items[0] || empty(),
17 | };
18 | 
19 | module.mutations = {
20 |   ...module.mutations,
21 |   setCurrentId(state, value) {
22 |     state.currentId = value;
23 |   },
24 | };
25 | 
26 | module.actions = {
27 |   ...module.actions,
28 |   patchCurrent({ getters, commit }, value) {
29 |     commit('patchItem', {
30 |       ...value,
31 |       id: getters.current.id,
32 |     });
33 |   },
34 | };
35 | 
36 | export default module;
37 | 


--------------------------------------------------------------------------------
/src/store/findReplace.js:
--------------------------------------------------------------------------------
 1 | export default {
 2 |   namespaced: true,
 3 |   state: {
 4 |     type: null,
 5 |     lastOpen: 0,
 6 |     findText: '',
 7 |     replaceText: '',
 8 |   },
 9 |   mutations: {
10 |     setType: (state, value) => {
11 |       state.type = value;
12 |     },
13 |     setLastOpen: (state) => {
14 |       state.lastOpen = Date.now();
15 |     },
16 |     setFindText: (state, value) => {
17 |       state.findText = value;
18 |     },
19 |     setReplaceText: (state, value) => {
20 |       state.replaceText = value;
21 |     },
22 |   },
23 |   actions: {
24 |     open({ commit }, { type, findText }) {
25 |       commit('setType', type);
26 |       if (findText) {
27 |         commit('setFindText', findText);
28 |       }
29 |       commit('setLastOpen');
30 |     },
31 |   },
32 | };
33 | 


--------------------------------------------------------------------------------
/src/store/folder.js:
--------------------------------------------------------------------------------
1 | import moduleTemplate from './moduleTemplate';
2 | import empty from '../data/empties/emptyFolder';
3 | 
4 | const module = moduleTemplate(empty);
5 | 
6 | export default module;
7 | 


--------------------------------------------------------------------------------
/src/store/modal.js:
--------------------------------------------------------------------------------
 1 | export default {
 2 |   namespaced: true,
 3 |   state: {
 4 |     stack: [],
 5 |     hidden: false,
 6 |   },
 7 |   mutations: {
 8 |     setStack: (state, value) => {
 9 |       state.stack = value;
10 |     },
11 |     setHidden: (state, value) => {
12 |       state.hidden = value;
13 |     },
14 |   },
15 |   getters: {
16 |     config: ({ hidden, stack }) => !hidden && stack[0],
17 |   },
18 |   actions: {
19 |     async open({ commit, state }, param) {
20 |       const config = typeof param === 'object' ? { ...param } : { type: param };
21 |       try {
22 |         return await new Promise((resolve, reject) => {
23 |           config.resolve = resolve;
24 |           config.reject = reject;
25 |           commit('setStack', [config, ...state.stack]);
26 |         });
27 |       } finally {
28 |         commit('setStack', state.stack.filter((otherConfig => otherConfig !== config)));
29 |       }
30 |     },
31 |     async hideUntil({ commit }, promise) {
32 |       try {
33 |         commit('setHidden', true);
34 |         return await promise;
35 |       } finally {
36 |         commit('setHidden', false);
37 |       }
38 |     },
39 |   },
40 | };
41 | 


--------------------------------------------------------------------------------
/src/store/moduleTemplate.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue';
 2 | import utils from '../services/utils';
 3 | 
 4 | export default (empty, simpleHash = false) => {
 5 |   // Use Date.now() as a simple hash function, which is ok for not-synced types
 6 |   const hashFunc = simpleHash ? Date.now : item => utils.getItemHash(item);
 7 | 
 8 |   return {
 9 |     namespaced: true,
10 |     state: {
11 |       itemsById: {},
12 |     },
13 |     getters: {
14 |       items: ({ itemsById }) => Object.values(itemsById),
15 |     },
16 |     mutations: {
17 |       setItem(state, value) {
18 |         const item = Object.assign(empty(value.id), value);
19 |         if (!item.hash || !simpleHash) {
20 |           item.hash = hashFunc(item);
21 |         }
22 |         Vue.set(state.itemsById, item.id, item);
23 |       },
24 |       patchItem(state, patch) {
25 |         const item = state.itemsById[patch.id];
26 |         if (item) {
27 |           Object.assign(item, patch);
28 |           item.hash = hashFunc(item);
29 |           Vue.set(state.itemsById, item.id, item);
30 |           return true;
31 |         }
32 |         return false;
33 |       },
34 |       deleteItem(state, id) {
35 |         Vue.delete(state.itemsById, id);
36 |       },
37 |     },
38 |     actions: {},
39 |   };
40 | };
41 | 


--------------------------------------------------------------------------------
/src/store/syncedContent.js:
--------------------------------------------------------------------------------
 1 | import moduleTemplate from './moduleTemplate';
 2 | import empty from '../data/empties/emptySyncedContent';
 3 | 
 4 | const module = moduleTemplate(empty, true);
 5 | 
 6 | module.getters = {
 7 |   ...module.getters,
 8 |   current: ({ itemsById }, getters, rootState, rootGetters) =>
 9 |     itemsById[`${rootGetters['file/current'].id}/syncedContent`] || empty(),
10 | };
11 | 
12 | export default module;
13 | 


--------------------------------------------------------------------------------
/src/store/userInfo.js:
--------------------------------------------------------------------------------
 1 | import Vue from 'vue';
 2 | 
 3 | export default {
 4 |   namespaced: true,
 5 |   state: {
 6 |     itemsById: {},
 7 |   },
 8 |   mutations: {
 9 |     setItem: ({ itemsById }, item) => {
10 |       const itemToSet = {
11 |         ...item,
12 |       };
13 |       const existingItem = itemsById[item.id];
14 |       if (existingItem) {
15 |         if (!itemToSet.name) {
16 |           itemToSet.name = existingItem.name;
17 |         }
18 |         if (!itemToSet.imageUrl) {
19 |           itemToSet.imageUrl = existingItem.imageUrl;
20 |         }
21 |       }
22 |       Vue.set(itemsById, item.id, itemToSet);
23 |     },
24 |   },
25 | };
26 | 


--------------------------------------------------------------------------------
/src/styles/fonts.scss:
--------------------------------------------------------------------------------
 1 | @font-face {
 2 |   font-family: 'Lato';
 3 |   font-style: normal;
 4 |   font-weight: 400;
 5 |   src: url('../assets/fonts/lato-normal.woff') format('woff');
 6 | }
 7 | 
 8 | @font-face {
 9 |   font-family: 'Lato';
10 |   font-style: italic;
11 |   font-weight: 400;
12 |   src: url('../assets/fonts/lato-normal-italic.woff') format('woff');
13 | }
14 | 
15 | @font-face {
16 |   font-family: 'Lato';
17 |   font-style: normal;
18 |   font-weight: 600;
19 |   src: url('../assets/fonts/lato-black.woff') format('woff');
20 | }
21 | 
22 | @font-face {
23 |   font-family: 'Lato';
24 |   font-style: italic;
25 |   font-weight: 600;
26 |   src: url('../assets/fonts/lato-black-italic.woff') format('woff');
27 | }
28 | 
29 | @font-face {
30 |   font-family: 'Roboto Mono';
31 |   font-style: normal;
32 |   font-weight: 400;
33 |   src: url('../assets/fonts/RobotoMono-Regular.woff') format('woff');
34 | }
35 | 
36 | @font-face {
37 |   font-family: 'Roboto Mono';
38 |   font-style: normal;
39 |   font-weight: 600;
40 |   src: url('../assets/fonts/RobotoMono-Bold.woff') format('woff');
41 | }
42 | 


--------------------------------------------------------------------------------
/src/styles/index.js:
--------------------------------------------------------------------------------
1 | import 'katex/dist/katex.css';
2 | import './fonts.scss';
3 | import './prism.scss';
4 | import './base.scss';
5 | 


--------------------------------------------------------------------------------
/src/styles/prism.scss:
--------------------------------------------------------------------------------
 1 | .token.pre.gfm,
 2 | .prism {
 3 |   * {
 4 |     font-weight: inherit !important;
 5 |   }
 6 | 
 7 |   .token.comment,
 8 |   .token.prolog,
 9 |   .token.doctype,
10 |   .token.cdata {
11 |     color: #708090;
12 |   }
13 | 
14 |   .token.punctuation {
15 |     color: #999;
16 |   }
17 | 
18 |   .namespace {
19 |     opacity: 0.7;
20 |   }
21 | 
22 |   .token.property,
23 |   .token.tag,
24 |   .token.boolean,
25 |   .token.number,
26 |   .token.constant,
27 |   .token.symbol,
28 |   .token.deleted {
29 |     color: #905;
30 |   }
31 | 
32 |   .token.selector,
33 |   .token.attr-name,
34 |   .token.string,
35 |   .token.char,
36 |   .token.builtin,
37 |   .token.inserted {
38 |     color: #690;
39 |   }
40 | 
41 |   .token.operator,
42 |   .token.entity,
43 |   .token.url,
44 |   .language-css .token.string,
45 |   .style .token.string {
46 |     color: #a67f59;
47 |   }
48 | 
49 |   .token.atrule,
50 |   .token.attr-value,
51 |   .token.keyword {
52 |     color: #07a;
53 |   }
54 | 
55 |   .token.function {
56 |     color: #dd4a68;
57 |   }
58 | 
59 |   .token.regex,
60 |   .token.important,
61 |   .token.variable {
62 |     color: #e90;
63 |   }
64 | 
65 |   .token.important,
66 |   .token.bold {
67 |     font-weight: 500;
68 |   }
69 | 
70 |   .token.italic {
71 |     font-style: italic;
72 |   }
73 | }
74 | 


--------------------------------------------------------------------------------
/src/styles/variables.scss:
--------------------------------------------------------------------------------
 1 | $font-family-main: Lato, 'Helvetica Neue', Helvetica, sans-serif;
 2 | $font-family-monospace: 'Roboto Mono', 'Lucida Sans Typewriter', 'Lucida Console', monaco, Courrier, monospace;
 3 | $body-color-light: rgba(0, 0, 0, 0.75);
 4 | $body-color-dark: rgba(255, 255, 255, 0.75);
 5 | $code-bg: rgba(0, 0, 0, 0.05);
 6 | $line-height-base: 1.67;
 7 | $line-height-title: 1.33;
 8 | $font-size-monospace: 0.85em;
 9 | $highlighting-color: #ff0;
10 | $selection-highlighting-color: #ff9632;
11 | $info-bg: #ffad3326;
12 | $code-border-radius: 3px;
13 | $link-color: #0c93e4;
14 | $error-color: #f31;
15 | $border-radius-base: 3px;
16 | $hr-color: rgba(128, 128, 128, 0.33);
17 | $navbar-bg: #2c2c2c;
18 | $navbar-color: mix($navbar-bg, #fff, 33%);
19 | $navbar-hover-color: #fff;
20 | $navbar-hover-background: rgba(255, 255, 255, 0.1);
21 | 
22 | $editor-background-light: #fff;
23 | $editor-background-dark: #1e1e1e;
24 | 
25 | $editor-color-light: rgba(0, 0, 0, 0.8);
26 | $editor-color-light-low: #000;
27 | $editor-color-light-high: rgba(0, 0, 0, 0.28);
28 | $editor-color-light-blockquote: rgba(0, 0, 0, 0.48);
29 | 
30 | $editor-color-dark: rgba(255, 255, 255, 0.8);
31 | $editor-color-dark-low: #fff;
32 | $editor-color-dark-high: rgba(255, 255, 255, 0.28);
33 | $editor-color-dark-blockquote: rgba(255, 255, 255, 0.48);
34 | 
35 | $editor-font-weight-base: 400;
36 | $editor-font-weight-bold: 600;
37 | 


--------------------------------------------------------------------------------
/static/landing/abc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/static/landing/abc.png


--------------------------------------------------------------------------------
/static/landing/discussion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/static/landing/discussion.png


--------------------------------------------------------------------------------
/static/landing/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/static/landing/favicon.ico


--------------------------------------------------------------------------------
/static/landing/gfm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/static/landing/gfm.png


--------------------------------------------------------------------------------
/static/landing/katex.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/static/landing/katex.gif


--------------------------------------------------------------------------------
/static/landing/mermaid.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/static/landing/mermaid.gif


--------------------------------------------------------------------------------
/static/landing/navigation-bar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/static/landing/navigation-bar.png


--------------------------------------------------------------------------------
/static/landing/providers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/static/landing/providers.png


--------------------------------------------------------------------------------
/static/landing/scroll-sync.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/static/landing/scroll-sync.gif


--------------------------------------------------------------------------------
/static/landing/smart-layout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/static/landing/smart-layout.png


--------------------------------------------------------------------------------
/static/landing/syntax-highlighting.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/static/landing/syntax-highlighting.gif


--------------------------------------------------------------------------------
/static/landing/twemoji.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/static/landing/twemoji.png


--------------------------------------------------------------------------------
/static/landing/workspace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/benweet/stackedit/6dce2a5e36b755a0c244522b48a06c91a2df0f59/static/landing/workspace.png


--------------------------------------------------------------------------------
/static/oauth2/callback.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html>
 3 |   <body>
 4 |     <script>
 5 |       var origin = location.protocol + '//' + location.host;
 6 |       (window.opener || window.parent).postMessage(location.hash || location.search, origin);
 7 |     </script>
 8 |   </body>
 9 | </html>
10 | 


--------------------------------------------------------------------------------
/static/sitemap.xml:
--------------------------------------------------------------------------------
 1 | <?xml version="1.0" encoding="UTF-8"?>
 2 | <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
 3 |     <url>
 4 |         <loc>https://stackedit.io/</loc>
 5 |         <changefreq>weekly</changefreq>
 6 |         <priority>1.0</priority>
 7 |     </url>
 8 |     <url>
 9 |         <loc>https://stackedit.io/app</loc>
10 |         <changefreq>weekly</changefreq>
11 |         <priority>1.0</priority>
12 |     </url>
13 |     <url>
14 |         <loc>https://community.stackedit.io/</loc>
15 |         <changefreq>weekly</changefreq>
16 |         <priority>0.8</priority>
17 |     </url>
18 |     <url>
19 |         <loc>https://stackedit.io/privacy_policy.html</loc>
20 |         <changefreq>monthly</changefreq>
21 |         <priority>0.6</priority>
22 |     </url>
23 | </urlset>
24 | 


--------------------------------------------------------------------------------
/test/unit/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 |   "env": {
3 |     "jest": true
4 |   },
5 |   "extends": [
6 |     "../../.eslintrc.js"
7 |   ]
8 | }
9 | 


--------------------------------------------------------------------------------
/test/unit/jest.conf.js:
--------------------------------------------------------------------------------
 1 | const path = require('path');
 2 | 
 3 | module.exports = {
 4 |   rootDir: path.resolve(__dirname, '../../'),
 5 |   moduleFileExtensions: [
 6 |     'js',
 7 |     'json',
 8 |     'vue',
 9 |   ],
10 |   moduleNameMapper: {
11 |     '\\.(css|scss)
#39;: 'identity-obj-proxy',
12 |     '^!raw-loader!': 'identity-obj-proxy',
13 |     '^worker-loader!\\./templateWorker\\.js
#39;: '<rootDir>/test/unit/mocks/templateWorkerMock',
14 |   },
15 |   transform: {
16 |     '^.+\\.js
#39;: '<rootDir>/node_modules/babel-jest',
17 |     '.*\\.(vue)
#39;: '<rootDir>/node_modules/vue-jest',
18 |     '.*\\.(yml|html|md)
#39;: 'jest-raw-loader',
19 |   },
20 |   snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
21 |   setupFiles: [
22 |     '<rootDir>/test/unit/setup',
23 |   ],
24 |   coverageDirectory: '<rootDir>/test/unit/coverage',
25 |   collectCoverageFrom: [
26 |     'src/**/*.{js,vue}',
27 |     '!src/main.js',
28 |     '!**/node_modules/**',
29 |   ],
30 |   globals: {
31 |     NODE_ENV: 'production',
32 |   },
33 | };
34 | 


--------------------------------------------------------------------------------
/test/unit/mocks/cryptoMock.js:
--------------------------------------------------------------------------------
1 | window.crypto = {
2 |   getRandomValues(array) {
3 |     for (let i = 0; i < array.length; i += 1) {
4 |       array[i] = Math.floor(Math.random() * 1000000);
5 |     }
6 |   },
7 | };
8 | 


--------------------------------------------------------------------------------
/test/unit/mocks/localStorageMock.js:
--------------------------------------------------------------------------------
 1 | const store = {};
 2 | window.localStorage = {
 3 |   getItem(key) {
 4 |     return store[key] || null;
 5 |   },
 6 |   setItem(key, value) {
 7 |     store[key] = value.toString();
 8 |   },
 9 | };
10 | 


--------------------------------------------------------------------------------
/test/unit/mocks/mutationObserverMock.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable class-methods-use-this */
2 | class MutationObserver {
3 |   observe() {
4 |   }
5 | }
6 | window.MutationObserver = MutationObserver;
7 | 


--------------------------------------------------------------------------------
/test/unit/mocks/templateWorkerMock.js:
--------------------------------------------------------------------------------
1 | module.exports = 'test-file-stub';
2 | 


--------------------------------------------------------------------------------
/test/unit/setup.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import './mocks/cryptoMock';
3 | import './mocks/mutationObserverMock';
4 | 
5 | Vue.config.productionTip = false;
6 | 


--------------------------------------------------------------------------------
/test/unit/specs/components/ButtonBar.spec.js:
--------------------------------------------------------------------------------
 1 | import ButtonBar from '../../../../src/components/ButtonBar';
 2 | import store from '../../../../src/store';
 3 | import specUtils from '../specUtils';
 4 | 
 5 | describe('ButtonBar.vue', () => {
 6 |   it('should toggle the navigation bar', async () => specUtils.checkToggler(
 7 |     ButtonBar,
 8 |     wrapper => wrapper.find('.button-bar__button--navigation-bar-toggler').trigger('click'),
 9 |     () => store.getters['data/layoutSettings'].showNavigationBar,
10 |     'toggleNavigationBar',
11 |   ));
12 | 
13 |   it('should toggle the side preview', async () => specUtils.checkToggler(
14 |     ButtonBar,
15 |     wrapper => wrapper.find('.button-bar__button--side-preview-toggler').trigger('click'),
16 |     () => store.getters['data/layoutSettings'].showSidePreview,
17 |     'toggleSidePreview',
18 |   ));
19 | 
20 |   it('should toggle the editor', async () => specUtils.checkToggler(
21 |     ButtonBar,
22 |     wrapper => wrapper.find('.button-bar__button--editor-toggler').trigger('click'),
23 |     () => store.getters['data/layoutSettings'].showEditor,
24 |     'toggleEditor',
25 |   ));
26 | 
27 |   it('should toggle the focus mode', async () => specUtils.checkToggler(
28 |     ButtonBar,
29 |     wrapper => wrapper.find('.button-bar__button--focus-mode-toggler').trigger('click'),
30 |     () => store.getters['data/layoutSettings'].focusMode,
31 |     'toggleFocusMode',
32 |   ));
33 | 
34 |   it('should toggle the scroll sync', async () => specUtils.checkToggler(
35 |     ButtonBar,
36 |     wrapper => wrapper.find('.button-bar__button--scroll-sync-toggler').trigger('click'),
37 |     () => store.getters['data/layoutSettings'].scrollSync,
38 |     'toggleScrollSync',
39 |   ));
40 | 
41 |   it('should toggle the status bar', async () => specUtils.checkToggler(
42 |     ButtonBar,
43 |     wrapper => wrapper.find('.button-bar__button--status-bar-toggler').trigger('click'),
44 |     () => store.getters['data/layoutSettings'].showStatusBar,
45 |     'toggleStatusBar',
46 |   ));
47 | });
48 | 


--------------------------------------------------------------------------------
/test/unit/specs/components/ContextMenu.spec.js:
--------------------------------------------------------------------------------
 1 | import { shallowMount } from '@vue/test-utils';
 2 | import ContextMenu from '../../../../src/components/ContextMenu';
 3 | import store from '../../../../src/store';
 4 | import '../specUtils';
 5 | 
 6 | const mount = () => shallowMount(ContextMenu, { store });
 7 | 
 8 | describe('ContextMenu.vue', () => {
 9 |   const name = 'Name';
10 |   const makeOptions = () => ({
11 |     coordinates: {
12 |       left: 0,
13 |       top: 0,
14 |     },
15 |     items: [{ name }],
16 |   });
17 | 
18 |   it('should open/close itself', async () => {
19 |     const wrapper = mount();
20 |     expect(wrapper.contains('.context-menu__item')).toEqual(false);
21 |     setTimeout(() => wrapper.find('.context-menu__item').trigger('click'), 1);
22 |     const item = await store.dispatch('contextMenu/open', makeOptions());
23 |     expect(item.name).toEqual(name);
24 |   });
25 | 
26 |   it('should cancel itself', async () => {
27 |     const wrapper = mount();
28 |     setTimeout(() => wrapper.trigger('click'), 1);
29 |     const item = await store.dispatch('contextMenu/open', makeOptions());
30 |     expect(item).toEqual(null);
31 |   });
32 | });
33 | 


--------------------------------------------------------------------------------
/test/unit/specs/components/NavigationBar.spec.js:
--------------------------------------------------------------------------------
 1 | import NavigationBar from '../../../../src/components/NavigationBar';
 2 | import store from '../../../../src/store';
 3 | import specUtils from '../specUtils';
 4 | 
 5 | describe('NavigationBar.vue', () => {
 6 |   it('should toggle the explorer', async () => specUtils.checkToggler(
 7 |     NavigationBar,
 8 |     wrapper => wrapper.find('.navigation-bar__button--explorer-toggler').trigger('click'),
 9 |     () => store.getters['data/layoutSettings'].showExplorer,
10 |     'toggleExplorer',
11 |   ));
12 | 
13 |   it('should toggle the side bar', async () => specUtils.checkToggler(
14 |     NavigationBar,
15 |     wrapper => wrapper.find('.navigation-bar__button--stackedit').trigger('click'),
16 |     () => store.getters['data/layoutSettings'].showSideBar,
17 |     'toggleSideBar',
18 |   ));
19 | });
20 | 


--------------------------------------------------------------------------------
/test/unit/specs/components/Notification.spec.js:
--------------------------------------------------------------------------------
 1 | import { shallowMount } from '@vue/test-utils';
 2 | import Notification from '../../../../src/components/Notification';
 3 | import store from '../../../../src/store';
 4 | import '../specUtils';
 5 | 
 6 | const mount = () => shallowMount(Notification, { store });
 7 | 
 8 | describe('Notification.vue', () => {
 9 |   it('should autoclose itself', async () => {
10 |     const wrapper = mount();
11 |     expect(wrapper.contains('.notification__item')).toBe(false);
12 |     store.dispatch('notification/showItem', {
13 |       type: 'info',
14 |       content: 'Test',
15 |       timeout: 10,
16 |     });
17 |     expect(wrapper.contains('.notification__item')).toBe(true);
18 |     await new Promise(resolve => setTimeout(resolve, 10));
19 |     expect(wrapper.contains('.notification__item')).toBe(false);
20 |   });
21 | 
22 |   it('should show messages from top to bottom', async () => {
23 |     const wrapper = mount();
24 |     store.dispatch('notification/info', 'Test 1');
25 |     store.dispatch('notification/info', 'Test 2');
26 |     const items = wrapper.findAll('.notification__item');
27 |     expect(items.length).toEqual(2);
28 |     expect(items.at(0).text()).toMatch(/Test 1/);
29 |     expect(items.at(1).text()).toMatch(/Test 2/);
30 |   });
31 | 
32 |   it('should not open the same message twice', async () => {
33 |     const wrapper = mount();
34 |     store.dispatch('notification/info', 'Test');
35 |     store.dispatch('notification/info', 'Test');
36 |     expect(wrapper.findAll('.notification__item').length).toEqual(1);
37 |   });
38 | });
39 | 


--------------------------------------------------------------------------------
/test/unit/specs/specUtils.js:
--------------------------------------------------------------------------------
 1 | import { shallowMount } from '@vue/test-utils';
 2 | import store from '../../../src/store';
 3 | import utils from '../../../src/services/utils';
 4 | import '../../../src/icons';
 5 | import '../../../src/components/common/vueGlobals';
 6 | 
 7 | const clone = object => JSON.parse(JSON.stringify(object));
 8 | 
 9 | const deepAssign = (target, origin) => {
10 |   Object.entries(origin).forEach(([key, value]) => {
11 |     const type = Object.prototype.toString.call(value);
12 |     if (type === '[object Object]' && Object.keys(value).length) {
13 |       deepAssign(target[key], value);
14 |     } else {
15 |       target[key] = value;
16 |     }
17 |   });
18 | };
19 | 
20 | const freshState = clone(store.state);
21 | 
22 | beforeEach(() => {
23 |   // Restore store state before each test
24 |   deepAssign(store.state, clone(freshState));
25 | });
26 | 
27 | export default {
28 |   async checkToggler(Component, toggler, checker, featureId) {
29 |     const wrapper = shallowMount(Component, { store });
30 |     const valueBefore = checker();
31 |     toggler(wrapper);
32 |     const valueAfter = checker();
33 |     expect(valueAfter).toEqual(!valueBefore);
34 |     await this.expectBadge(featureId);
35 |   },
36 |   async resolveModal(type) {
37 |     const config = store.getters['modal/config'];
38 |     expect(config).toBeTruthy();
39 |     expect(config.type).toEqual(type);
40 |     config.resolve();
41 |     await new Promise(resolve => setTimeout(resolve, 1));
42 |   },
43 |   getContextMenuItem(name) {
44 |     return utils.someResult(store.state.contextMenu.items, item => item.name === name && item);
45 |   },
46 |   async resolveContextMenu(name) {
47 |     const item = this.getContextMenuItem(name);
48 |     expect(item).toBeTruthy();
49 |     store.state.contextMenu.resolve(item);
50 |     await new Promise(resolve => setTimeout(resolve, 1));
51 |   },
52 |   async expectBadge(featureId, isEarned = true) {
53 |     await new Promise(resolve => setTimeout(resolve, 1));
54 |     expect(store.getters['data/allBadges'].filter(badge => badge.featureId === featureId)[0]).toMatchObject({
55 |       isEarned,
56 |     });
57 |   },
58 | };
59 | 


--------------------------------------------------------------------------------