├── .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"> — {{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:  53 | 54 | A sized image:  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">☐</span>'; 29 | } else { 30 | checkbox.content = '<span class="task-list-item-checkbox checked" type="checkbox">☑</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 | --------------------------------------------------------------------------------