├── public
├── default.md
├── uploads
│ └── .gitkeep
├── views
│ ├── codimd
│ │ ├── footer.ejs
│ │ └── head.ejs
│ ├── index
│ │ ├── footer.ejs
│ │ ├── header.ejs
│ │ ├── foot.ejs
│ │ └── head.ejs
│ ├── includes
│ │ ├── header.ejs
│ │ └── scripts.ejs
│ ├── index.ejs
│ ├── codimd.ejs
│ ├── shared
│ │ ├── ga.ejs
│ │ ├── disqus.ejs
│ │ ├── polyfill.ejs
│ │ ├── revision-modal.ejs
│ │ ├── refresh-modal.ejs
│ │ └── pandoc-export-modal.ejs
│ └── error.ejs
├── favicon.png
├── screenshot.png
├── apple-touch-icon.png
├── codimd-icon-1024.png
├── js
│ ├── lib
│ │ ├── editor
│ │ │ ├── config.js
│ │ │ └── constants.js
│ │ ├── modeType.js
│ │ ├── appState.js
│ │ ├── renderer
│ │ │ ├── fretboard
│ │ │ │ └── svg
│ │ │ │ │ ├── dotWideMiddle.svg
│ │ │ │ │ ├── dotWideLeft.svg
│ │ │ │ │ ├── dot.svg
│ │ │ │ │ ├── dotWideRight.svg
│ │ │ │ │ ├── dotEmpty.svg
│ │ │ │ │ ├── dotEmpty_h.svg
│ │ │ │ │ ├── dot_h.svg
│ │ │ │ │ ├── string_o.svg
│ │ │ │ │ ├── string_x.svg
│ │ │ │ │ ├── fretb_vert_4.svg
│ │ │ │ │ ├── fretb_vert_5.svg
│ │ │ │ │ ├── fretb_horiz_5.svg
│ │ │ │ │ ├── fretb_horiz_6.svg
│ │ │ │ │ ├── fretb_horiz_7.svg
│ │ │ │ │ ├── fretb_vert_7.svg
│ │ │ │ │ ├── fretb_vert_9.svg
│ │ │ │ │ ├── fretb_vert_12.svg
│ │ │ │ │ ├── fretb_vert_15.svg
│ │ │ │ │ ├── number7.svg
│ │ │ │ │ ├── number7_h.svg
│ │ │ │ │ ├── number1.svg
│ │ │ │ │ ├── number1_h.svg
│ │ │ │ │ ├── number4.svg
│ │ │ │ │ └── number4_h.svg
│ │ │ └── csvpreview.js
│ │ ├── common
│ │ │ ├── constant.ejs
│ │ │ ├── metrics.ejs
│ │ │ └── login.js
│ │ ├── config
│ │ │ └── index.js
│ │ └── markdown
│ │ │ └── utils.js
│ ├── htmlExport.js
│ ├── mathjax-config-extra.js
│ ├── locale.js
│ └── utils.js
├── fonts
│ ├── SourceCodePro-Black.eot
│ ├── SourceCodePro-Black.ttf
│ ├── SourceCodePro-Bold.eot
│ ├── SourceCodePro-Bold.ttf
│ ├── SourceCodePro-Bold.woff
│ ├── SourceCodePro-Light.eot
│ ├── SourceCodePro-Light.ttf
│ ├── SourceSansPro-Black.eot
│ ├── SourceSansPro-Black.ttf
│ ├── SourceSansPro-Bold.eot
│ ├── SourceSansPro-Bold.ttf
│ ├── SourceSansPro-Bold.woff
│ ├── SourceSansPro-Light.eot
│ ├── SourceSansPro-Light.ttf
│ ├── SourceSerifPro-Bold.eot
│ ├── SourceSerifPro-Bold.ttf
│ ├── SourceCodePro-Black.woff
│ ├── SourceCodePro-Light.woff
│ ├── SourceCodePro-Medium.eot
│ ├── SourceCodePro-Medium.ttf
│ ├── SourceCodePro-Medium.woff
│ ├── SourceCodePro-Regular.eot
│ ├── SourceCodePro-Regular.ttf
│ ├── SourceSansPro-Black.woff
│ ├── SourceSansPro-Italic.eot
│ ├── SourceSansPro-Italic.ttf
│ ├── SourceSansPro-Italic.woff
│ ├── SourceSansPro-Light.woff
│ ├── SourceSansPro-Regular.eot
│ ├── SourceSansPro-Regular.ttf
│ ├── SourceSerifPro-Bold.woff
│ ├── SourceCodePro-ExtraLight.eot
│ ├── SourceCodePro-ExtraLight.ttf
│ ├── SourceCodePro-Regular.woff
│ ├── SourceCodePro-Semibold.eot
│ ├── SourceCodePro-Semibold.ttf
│ ├── SourceCodePro-Semibold.woff
│ ├── SourceSansPro-BoldItalic.eot
│ ├── SourceSansPro-BoldItalic.ttf
│ ├── SourceSansPro-ExtraLight.eot
│ ├── SourceSansPro-ExtraLight.ttf
│ ├── SourceSansPro-Regular.woff
│ ├── SourceSansPro-Semibold.eot
│ ├── SourceSansPro-Semibold.ttf
│ ├── SourceSansPro-Semibold.woff
│ ├── SourceSerifPro-Regular.eot
│ ├── SourceSerifPro-Regular.ttf
│ ├── SourceSerifPro-Regular.woff
│ ├── SourceSerifPro-Semibold.eot
│ ├── SourceSerifPro-Semibold.ttf
│ ├── SourceSerifPro-Semibold.woff
│ ├── SourceCodePro-ExtraLight.woff
│ ├── SourceSansPro-BlackItalic.eot
│ ├── SourceSansPro-BlackItalic.ttf
│ ├── SourceSansPro-BlackItalic.woff
│ ├── SourceSansPro-BoldItalic.woff
│ ├── SourceSansPro-ExtraLight.woff
│ ├── SourceSansPro-LightItalic.eot
│ ├── SourceSansPro-LightItalic.ttf
│ ├── SourceSansPro-LightItalic.woff
│ ├── SourceSansPro-SemiboldItalic.eot
│ ├── SourceSansPro-SemiboldItalic.ttf
│ ├── SourceSansPro-SemiboldItalic.woff
│ ├── SourceSansPro-ExtraLightItalic.eot
│ ├── SourceSansPro-ExtraLightItalic.ttf
│ └── SourceSansPro-ExtraLightItalic.woff
├── markdown-lint
│ ├── images
│ │ ├── mark-error.png
│ │ ├── mark-multiple.png
│ │ ├── mark-warning.png
│ │ ├── message-error.png
│ │ └── message-warning.png
│ └── css
│ │ └── lint.css
├── vendor
│ ├── jquery-ui
│ │ └── images
│ │ │ ├── ui-icons_222222_256x240.png
│ │ │ ├── ui-icons_2e83ff_256x240.png
│ │ │ ├── ui-icons_454545_256x240.png
│ │ │ ├── ui-icons_888888_256x240.png
│ │ │ ├── ui-icons_cd0a0a_256x240.png
│ │ │ ├── ui-bg_flat_0_aaaaaa_40x100.png
│ │ │ ├── ui-bg_flat_75_ffffff_40x100.png
│ │ │ ├── ui-bg_glass_55_fbf9ee_1x400.png
│ │ │ ├── ui-bg_glass_65_ffffff_1x400.png
│ │ │ ├── ui-bg_glass_75_dadada_1x400.png
│ │ │ ├── ui-bg_glass_75_e6e6e6_1x400.png
│ │ │ ├── ui-bg_glass_95_fef1ec_1x400.png
│ │ │ └── ui-bg_highlight-soft_75_cccccc_1x100.png
│ ├── ot
│ │ ├── compress.sh
│ │ ├── socketio-adapter.js
│ │ └── wrapped-operation.js
│ └── codemirror-spell-checker
│ │ └── spell-checker.min.css
├── css
│ ├── google-font.css
│ ├── center.css
│ ├── codemirror-extend
│ │ ├── one-dark.css
│ │ ├── tomorrow-night-bright.css
│ │ ├── ayu-dark.css
│ │ └── ayu-mirage.css
│ ├── site.css
│ └── slide-preview.css
├── .eslintrc.js
├── images
│ └── mattermost-logo.svg
└── docs
│ └── privacy.md.example
├── .nvmrc
├── Aptfile
├── Procfile
├── config.js
├── .buildpacks
├── bin
├── heroku_start.sh
├── heroku
└── setup
├── FUNDING.json
├── utils
└── string.js
├── .github
├── tests
│ ├── pull-request.json
│ └── README.md
└── workflows
│ ├── check-release.yml
│ └── build.yml
├── .dockerignore
├── lib
├── middleware
│ ├── codiMDVersion.js
│ ├── checkURIValid.js
│ ├── tooBusy.js
│ └── redirectWithoutTrailingSlashes.js
├── migrations
│ ├── 20240114120250-revision-add-index.js
│ ├── 20180525153000-user-add-delete-token.js
│ ├── 20180306150303-fix-enum.js
│ ├── 20150515125813-create-temp.js
│ ├── 20180209120907-longtext-of-authorship.js
│ ├── 20150508114741-create-notes.js
│ ├── 20150504155329-create-users.js
│ ├── 20161009040430-support-delete-note.js
│ ├── 20180326103000-use-text-in-tokens.js
│ ├── 20200104215332-remove-temp-table.js
│ ├── 20150915153700-change-notes-title-to-text.js
│ ├── 20160515114000-user-add-tokens.js
│ ├── 20171009121200-longtext-for-mysql.js
│ ├── 20160420180355-note-add-alias.js
│ ├── 20160112220142-note-add-lastchange.js
│ ├── 20161201050312-support-email-signin.js
│ ├── 20160607060246-support-revision.js
│ ├── 20160703062241-support-authorship.js
│ └── 20150702001020-update-to-0_3_1.js
├── config
│ ├── enum.js
│ ├── defaultSSL.js
│ └── utils.js
├── ot
│ ├── index.js
│ ├── server.js
│ └── wrapped-operation.js
├── metrics.js
├── errorPage
│ └── index.js
├── logger.js
├── imageRouter
│ ├── imgur.js
│ ├── lutim.js
│ ├── azure.js
│ ├── minio.js
│ ├── filesystem.js
│ ├── index.js
│ └── s3.js
├── models
│ ├── author.js
│ └── index.js
├── auth
│ ├── twitter
│ │ └── index.js
│ ├── facebook
│ │ └── index.js
│ ├── bitbucket
│ │ └── index.js
│ ├── dropbox
│ │ └── index.js
│ ├── google
│ │ └── index.js
│ ├── oauth2
│ │ └── index.js
│ ├── gitlab
│ │ └── index.js
│ ├── mattermost
│ │ └── index.js
│ ├── openid
│ │ └── index.js
│ ├── index.js
│ └── utils.js
├── homepage
│ └── index.js
├── realtime
│ ├── realtimeSaveRevisionJob.js
│ ├── realtimeCleanDanglingUserJob.js
│ └── realtimeUpdateDirtyNoteJob.js
├── utils.js
├── status
│ └── index.js
├── letter-avatars.js
└── web
│ └── middleware
│ └── checkVersion.js
├── .sequelizerc.example
├── test
├── testDoubles
│ ├── realtimeJobStub.js
│ ├── loggerFake.js
│ ├── otFake.js
│ └── ProcessQueueFake.js
├── realtime
│ ├── utils.js
│ ├── cleanDanglingUser.test.js
│ ├── realtime.test.js
│ └── saveRevisionJob.test.js
├── auth
│ └── oauth2
│ │ └── strategy.test.js
└── letter-avatars.js
├── .editorconfig
├── webpack.dev.js
├── deployments
├── docker-entrypoint.sh
├── build.sh
├── Dockerfile
└── docker-compose.yml
├── babel.config.js
├── .gitignore
├── webpack.htmlexport.js
├── webpack.prod.js
├── .devcontainer
├── Dockerfile
├── docker-compose.yml
└── devcontainer.json
├── .mailmap
└── contribute
└── developer-certificate-of-origin
/public/default.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v16.20.2
2 |
--------------------------------------------------------------------------------
/Aptfile:
--------------------------------------------------------------------------------
1 | libvips-dev
2 |
--------------------------------------------------------------------------------
/public/uploads/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/views/codimd/footer.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/views/index/footer.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/views/index/header.ejs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: ./bin/heroku_start.sh
2 |
--------------------------------------------------------------------------------
/config.js:
--------------------------------------------------------------------------------
1 | const config = require('./lib/config')
2 |
3 | module.exports = config.db
4 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/favicon.png
--------------------------------------------------------------------------------
/public/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/screenshot.png
--------------------------------------------------------------------------------
/.buildpacks:
--------------------------------------------------------------------------------
1 | https://github.com/Scalingo/apt-buildpack
2 | https://github.com/Scalingo/nodejs-buildpack
3 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/codimd-icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/codimd-icon-1024.png
--------------------------------------------------------------------------------
/public/js/lib/editor/config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | docmaxlength: null
3 | }
4 |
5 | export default config
6 |
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Black.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Black.eot
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Black.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Bold.eot
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Bold.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Bold.woff
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Light.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Light.eot
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Light.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Black.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Black.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Black.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Black.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Bold.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Bold.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Bold.woff
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Light.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Light.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Light.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSerifPro-Bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Bold.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSerifPro-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Bold.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Black.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Black.woff
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Light.woff
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Medium.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Medium.eot
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Medium.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Medium.woff
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Regular.eot
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Regular.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Black.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Black.woff
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Italic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Italic.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Italic.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Italic.woff
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Light.woff
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Regular.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Regular.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSerifPro-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Bold.woff
--------------------------------------------------------------------------------
/bin/heroku_start.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -euo pipefail
4 |
5 | CMD_DB_URL="$DATABASE_URL" CMD_PORT="$PORT" npm run start
6 |
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-ExtraLight.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-ExtraLight.eot
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-ExtraLight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-ExtraLight.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Regular.woff
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Semibold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Semibold.eot
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Semibold.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-Semibold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-Semibold.woff
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-BoldItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-BoldItalic.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-BoldItalic.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-ExtraLight.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-ExtraLight.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-ExtraLight.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-ExtraLight.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Regular.woff
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Semibold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Semibold.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Semibold.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-Semibold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-Semibold.woff
--------------------------------------------------------------------------------
/public/fonts/SourceSerifPro-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Regular.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSerifPro-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Regular.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSerifPro-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Regular.woff
--------------------------------------------------------------------------------
/public/fonts/SourceSerifPro-Semibold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Semibold.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSerifPro-Semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Semibold.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSerifPro-Semibold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSerifPro-Semibold.woff
--------------------------------------------------------------------------------
/public/fonts/SourceCodePro-ExtraLight.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceCodePro-ExtraLight.woff
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-BlackItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-BlackItalic.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-BlackItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-BlackItalic.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-BlackItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-BlackItalic.woff
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-BoldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-BoldItalic.woff
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-ExtraLight.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-ExtraLight.woff
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-LightItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-LightItalic.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-LightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-LightItalic.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-LightItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-LightItalic.woff
--------------------------------------------------------------------------------
/public/markdown-lint/images/mark-error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/markdown-lint/images/mark-error.png
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-SemiboldItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-SemiboldItalic.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-SemiboldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-SemiboldItalic.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-SemiboldItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-SemiboldItalic.woff
--------------------------------------------------------------------------------
/public/markdown-lint/images/mark-multiple.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/markdown-lint/images/mark-multiple.png
--------------------------------------------------------------------------------
/public/markdown-lint/images/mark-warning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/markdown-lint/images/mark-warning.png
--------------------------------------------------------------------------------
/public/markdown-lint/images/message-error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/markdown-lint/images/message-error.png
--------------------------------------------------------------------------------
/FUNDING.json:
--------------------------------------------------------------------------------
1 | {
2 | "drips": {
3 | "ethereum": {
4 | "ownedBy": "0xEd37B84FD84A834886aC07693aF6A9cd35040002"
5 | }
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-ExtraLightItalic.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-ExtraLightItalic.eot
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-ExtraLightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-ExtraLightItalic.ttf
--------------------------------------------------------------------------------
/public/fonts/SourceSansPro-ExtraLightItalic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/fonts/SourceSansPro-ExtraLightItalic.woff
--------------------------------------------------------------------------------
/public/markdown-lint/images/message-warning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/markdown-lint/images/message-warning.png
--------------------------------------------------------------------------------
/public/vendor/jquery-ui/images/ui-icons_222222_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-icons_222222_256x240.png
--------------------------------------------------------------------------------
/public/vendor/jquery-ui/images/ui-icons_2e83ff_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-icons_2e83ff_256x240.png
--------------------------------------------------------------------------------
/public/vendor/jquery-ui/images/ui-icons_454545_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-icons_454545_256x240.png
--------------------------------------------------------------------------------
/public/vendor/jquery-ui/images/ui-icons_888888_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-icons_888888_256x240.png
--------------------------------------------------------------------------------
/public/vendor/jquery-ui/images/ui-icons_cd0a0a_256x240.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-icons_cd0a0a_256x240.png
--------------------------------------------------------------------------------
/utils/string.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | function stripTags (s) {
4 | return s.replace(RegExp('?[^<>]*>', 'gi'), '')
5 | }
6 |
7 | exports.stripTags = stripTags
8 |
--------------------------------------------------------------------------------
/public/vendor/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_flat_0_aaaaaa_40x100.png
--------------------------------------------------------------------------------
/public/vendor/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_flat_75_ffffff_40x100.png
--------------------------------------------------------------------------------
/public/vendor/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_glass_55_fbf9ee_1x400.png
--------------------------------------------------------------------------------
/public/vendor/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_glass_65_ffffff_1x400.png
--------------------------------------------------------------------------------
/public/vendor/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_glass_75_dadada_1x400.png
--------------------------------------------------------------------------------
/public/vendor/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_glass_75_e6e6e6_1x400.png
--------------------------------------------------------------------------------
/public/vendor/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_glass_95_fef1ec_1x400.png
--------------------------------------------------------------------------------
/public/vendor/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hackmdio/codimd/HEAD/public/vendor/jquery-ui/images/ui-bg_highlight-soft_75_cccccc_1x100.png
--------------------------------------------------------------------------------
/public/js/lib/modeType.js:
--------------------------------------------------------------------------------
1 | export default {
2 | edit: {
3 | name: 'edit'
4 | },
5 | view: {
6 | name: 'view'
7 | },
8 | both: {
9 | name: 'both'
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.github/tests/pull-request.json:
--------------------------------------------------------------------------------
1 | {
2 | "pull_request": {
3 | "head": {
4 | "ref": "release/1.2.3"
5 | },
6 | "base": {
7 | "ref": "master"
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/public/views/includes/header.ejs:
--------------------------------------------------------------------------------
1 | <% for (var css in htmlWebpackPlugin.files.css) { %>
2 |
3 | <% } %>
4 |
--------------------------------------------------------------------------------
/public/css/google-font.css:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,400italic,600,600italic,300italic,300|Source+Serif+Pro|Source+Code+Pro:400,300,500&subset=latin,latin-ext);
2 |
--------------------------------------------------------------------------------
/public/js/lib/appState.js:
--------------------------------------------------------------------------------
1 | import modeType from './modeType'
2 |
3 | const state = {
4 | syncscroll: true,
5 | currentMode: modeType.view,
6 | nightMode: false
7 | }
8 |
9 | export default state
10 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | .idea
2 | coverage
3 | node_modules/
4 |
5 | # ignore config files
6 | config.json
7 | .sequelizerc
8 |
9 | # ignore webpack build
10 | public/build
11 | public/views/build
12 |
13 | .nyc_output
14 | coverage/
15 |
--------------------------------------------------------------------------------
/public/js/htmlExport.js:
--------------------------------------------------------------------------------
1 | require('../css/github-extract.css')
2 | require('../css/markdown.css')
3 | require('../css/extra.css')
4 | require('../css/slide-preview.css')
5 | require('../css/google-font.css')
6 | require('../css/site.css')
7 |
--------------------------------------------------------------------------------
/.github/tests/README.md:
--------------------------------------------------------------------------------
1 | # Test github actions with act
2 |
3 | ```bash
4 | act pull_request --container-architecture linux/arm64 -e .github/tests/pull-request.json -j ch
5 | eck-release-pr -P ubuntu-latest=catthehacker/ubuntu:act-latest
6 | ```
7 |
--------------------------------------------------------------------------------
/lib/middleware/codiMDVersion.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const config = require('../config')
4 |
5 | module.exports = function (req, res, next) {
6 | res.set({
7 | 'CodiMD-Version': config.version
8 | })
9 | return next()
10 | }
11 |
--------------------------------------------------------------------------------
/public/views/includes/scripts.ejs:
--------------------------------------------------------------------------------
1 |
2 | <% for (var js in htmlWebpackPlugin.files.js) { %>
3 |
4 | <% } %>
5 |
--------------------------------------------------------------------------------
/bin/heroku:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | if [ ! -z "$DYNO" ]; then
6 | # setup config files
7 | cp .sequelizerc.example .sequelizerc
8 |
9 | cat << EOF > config.json
10 |
11 | {
12 | "production": {
13 | }
14 | }
15 |
16 | EOF
17 |
18 | fi
19 |
--------------------------------------------------------------------------------
/public/css/center.css:
--------------------------------------------------------------------------------
1 | html,
2 | body,
3 | .container-fluid {
4 | height: 98%;
5 | }
6 | .container-fluid {
7 | display: table;
8 | vertical-align: middle;
9 | }
10 | .vertical-center-row {
11 | display: table-cell;
12 | vertical-align: middle;
13 | }
--------------------------------------------------------------------------------
/public/js/mathjax-config-extra.js:
--------------------------------------------------------------------------------
1 | window.MathJax = {
2 | messageStyle: 'none',
3 | skipStartupTypeset: true,
4 | tex2jax: {
5 | inlineMath: [['$', '$'], ['\\(', '\\)']],
6 | processEscapes: true
7 | },
8 | TeX: {
9 | extensions: ['mhchem.js']
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/dotWideMiddle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vendor/ot/compress.sh:
--------------------------------------------------------------------------------
1 | uglifyjs --compress --mangle --output ot.min.js \
2 | ./text-operation.js \
3 | ./selection.js \
4 | ./wrapped-operation.js \
5 | ./undo-manager.js \
6 | ./client.js \
7 | ./codemirror-adapter.js \
8 | ./socketio-adapter.js \
9 | ./ajax-adapter.js \
10 | ./editor-client.js
--------------------------------------------------------------------------------
/.sequelizerc.example:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const config = require('./lib/config')
3 |
4 | module.exports = {
5 | config: path.resolve('config.js'),
6 | 'migrations-path': path.resolve('lib', 'migrations'),
7 | 'models-path': path.resolve('lib', 'models'),
8 | url: config.dbURL
9 | }
10 |
--------------------------------------------------------------------------------
/public/css/codemirror-extend/one-dark.css:
--------------------------------------------------------------------------------
1 | .cm-s-one-dark .CodeMirror-linenumber {
2 | color: #676767;
3 | }
4 |
5 | .cm-s-one-dark.CodeMirror-focused
6 | .CodeMirror-activeline
7 | .CodeMirror-gutter-elt {
8 | color: #b0b0b0;
9 | }
10 |
11 | .cm-s-one-dark .cm-comment {
12 | color: #818895;
13 | }
14 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/dotWideLeft.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/dot.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/migrations/20240114120250-revision-add-index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | up: (queryInterface, Sequelize) => {
5 | return queryInterface.addIndex('Revisions', ['noteId'], {})
6 | },
7 |
8 | down: (queryInterface, Sequelize) => {
9 | return queryInterface.removeIndex('Revisions', 'noteId')
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/dotWideRight.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%- include('index/head') %>
6 |
7 |
8 |
9 | <%- include('index/header') %>
10 | <%- include('index/body') %>
11 | <%- include('index/footer') %>
12 | <%- include('index/foot') %>
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/testDoubles/realtimeJobStub.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | class realtimeJobStub {
4 | start () {
5 | }
6 |
7 | stop () {
8 | }
9 | }
10 |
11 | exports.realtimeJobStub = realtimeJobStub
12 | exports.UpdateDirtyNoteJob = realtimeJobStub
13 | exports.CleanDanglingUserJob = realtimeJobStub
14 | exports.SaveRevisionJob = realtimeJobStub
15 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/dotEmpty.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/dotEmpty_h.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/dot_h.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/views/codimd.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%- include('codimd/head') %>
6 |
7 |
8 |
9 | <%- include('codimd/header') %>
10 | <%- include('codimd/body') %>
11 | <%- include('codimd/footer') %>
12 | <%- include('codimd/foot') %>
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test/testDoubles/loggerFake.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const sinon = require('sinon')
4 |
5 | function createFakeLogger () {
6 | return {
7 | error: sinon.stub(),
8 | warn: sinon.stub(),
9 | info: sinon.stub(),
10 | debug: sinon.stub(),
11 | log: sinon.stub()
12 | }
13 | }
14 |
15 | exports.createFakeLogger = createFakeLogger
16 |
--------------------------------------------------------------------------------
/lib/config/enum.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | exports.Environment = {
4 | development: 'development',
5 | production: 'production',
6 | test: 'test'
7 | }
8 |
9 | exports.Permission = {
10 | freely: 'freely',
11 | editable: 'editable',
12 | limited: 'limited',
13 | locked: 'locked',
14 | protected: 'protected',
15 | private: 'private'
16 | }
17 |
--------------------------------------------------------------------------------
/public/vendor/codemirror-spell-checker/spell-checker.min.css:
--------------------------------------------------------------------------------
1 | /**
2 | * codemirror-spell-checker v1.0.6
3 | * Copyright Next Step Webs, Inc.
4 | * @link https://github.com/NextStepWebs/codemirror-spell-checker
5 | * @license MIT
6 | */
7 | .CodeMirror .cm-spell-error:not(.cm-url):not(.cm-comment):not(.cm-tag):not(.cm-word){border-bottom:2px dotted rgba(255,0,0,.8)}
--------------------------------------------------------------------------------
/lib/middleware/checkURIValid.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const logger = require('../logger')
4 | const response = require('../response')
5 |
6 | module.exports = function (req, res, next) {
7 | try {
8 | decodeURIComponent(req.path)
9 | } catch (err) {
10 | logger.error(err)
11 | return response.errorBadRequest(req, res)
12 | }
13 | next()
14 | }
15 |
--------------------------------------------------------------------------------
/lib/middleware/tooBusy.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const toobusy = require('toobusy-js')
4 |
5 | const config = require('../config')
6 | const response = require('../response')
7 |
8 | toobusy.maxLag(config.responseMaxLag)
9 |
10 | module.exports = function (req, res, next) {
11 | if (toobusy()) {
12 | response.errorServiceUnavailable(req, res)
13 | } else {
14 | next()
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/ot/index.js:
--------------------------------------------------------------------------------
1 | exports.version = '0.0.15';
2 |
3 | exports.TextOperation = require('./text-operation');
4 | exports.SimpleTextOperation = require('./simple-text-operation');
5 | exports.Client = require('./client');
6 | exports.Server = require('./server');
7 | exports.Selection = require('./selection');
8 | exports.EditorSocketIOServer = require('./editor-socketio-server');
9 |
--------------------------------------------------------------------------------
/public/views/shared/ga.ejs:
--------------------------------------------------------------------------------
1 | <% if(typeof GA !== 'undefined' && GA) { %>
2 |
4 |
11 | <% } %>
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | trim_trailing_whitespace = true
7 | insert_final_newline = true
8 |
9 | [{*.html,*.ejs}]
10 | indent_style = space
11 | indent_size = 4
12 | trim_trailing_whitespace = true
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
17 | [{.travis.yml,npm-shrinkwrap.json,package.json}]
18 | indent_style = space
19 | indent_size = 2
20 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/string_o.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/metrics.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { Router } = require('express')
4 |
5 | const { wrap } = require('./utils')
6 |
7 | // load controller
8 | const statusController = require('./status')
9 | const appRouter = Router()
10 |
11 | // register route
12 | appRouter.get('/status', wrap(statusController.getStatus))
13 | appRouter.get('/metrics/codimd', wrap(statusController.getMetrics))
14 |
15 | exports.router = appRouter
16 |
--------------------------------------------------------------------------------
/lib/migrations/20180525153000-user-add-delete-token.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | up: function (queryInterface, Sequelize) {
4 | return queryInterface.addColumn('Users', 'deleteToken', {
5 | type: Sequelize.UUID,
6 | defaultValue: Sequelize.UUIDV4
7 | })
8 | },
9 |
10 | down: function (queryInterface, Sequelize) {
11 | return queryInterface.removeColumn('Users', 'deleteToken')
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/webpack.dev.js:
--------------------------------------------------------------------------------
1 | const common = require('./webpack.common.js')
2 | const htmlexport = require('./webpack.htmlexport')
3 | const merge = require('webpack-merge')
4 |
5 | module.exports = [
6 | // merge common config
7 | merge(common, {
8 | mode: 'development',
9 | devtool: 'cheap-module-eval-source-map'
10 | }),
11 | merge(htmlexport, {
12 | mode: 'development',
13 | devtool: 'cheap-module-eval-source-map'
14 | })]
15 |
--------------------------------------------------------------------------------
/deployments/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -euo pipefail
4 |
5 | if [[ "$#" -gt 0 ]]; then
6 | exec "$@"
7 | exit $?
8 | fi
9 |
10 | # check database and redis is ready
11 | pcheck -env CMD_DB_URL
12 |
13 | # run DB migrate
14 | NEED_MIGRATE=${CMD_AUTO_MIGRATE:=true}
15 |
16 | if [[ "$NEED_MIGRATE" = "true" ]] && [[ -f .sequelizerc ]] ; then
17 | npx sequelize db:migrate
18 | fi
19 |
20 | # start application
21 | node app.js
22 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', {
4 | targets: {
5 | node: '14'
6 | },
7 | useBuiltIns: 'usage',
8 | corejs: 3,
9 | modules: 'auto'
10 | }]
11 | ],
12 | plugins: [
13 | ['@babel/plugin-transform-runtime', {
14 | corejs: 3
15 | }],
16 | '@babel/plugin-transform-nullish-coalescing-operator',
17 | '@babel/plugin-transform-optional-chaining'
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/lib/config/defaultSSL.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('fs')
4 |
5 | function getFile (path) {
6 | if (fs.existsSync(path)) {
7 | return path
8 | }
9 | return undefined
10 | }
11 |
12 | module.exports = {
13 | sslKeyPath: getFile('/run/secrets/key.pem'),
14 | sslCertPath: getFile('/run/secrets/cert.pem'),
15 | sslCAPath: getFile('/run/secrets/ca.pem') !== undefined ? [getFile('/run/secrets/ca.pem')] : [],
16 | dhParamPath: getFile('/run/secrets/dhparam.pem')
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | composer.phar
3 | composer.lock
4 | .env.*.php
5 | .env.php
6 | .DS_Store
7 | .idea/
8 | Thumbs.db
9 | npm-debug.log
10 | hackmd_io
11 | newrelic_agent.log
12 | logs/
13 | tmp/
14 | backups/
15 | *.pid
16 | *.log
17 | *.sqlite
18 |
19 | # ignore config files
20 | config.json
21 | .sequelizerc
22 |
23 | # ignore webpack build
24 | public/build
25 | public/views/build
26 |
27 | public/uploads/*
28 | !public/uploads/.gitkeep
29 | /.nyc_output
30 | /coverage/
31 |
32 | .vscode/settings.json
--------------------------------------------------------------------------------
/lib/migrations/20180306150303-fix-enum.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | up: async function (queryInterface, Sequelize) {
5 | await queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private') })
6 | },
7 |
8 | down: async function (queryInterface, Sequelize) {
9 | await queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'locked', 'private') })
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/lib/migrations/20150515125813-create-temp.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | up: function (queryInterface, Sequelize) {
4 | return queryInterface.createTable('Temp', {
5 | id: {
6 | type: Sequelize.STRING,
7 | primaryKey: true
8 | },
9 | date: Sequelize.TEXT,
10 | createdAt: Sequelize.DATE,
11 | updatedAt: Sequelize.DATE
12 | })
13 | },
14 |
15 | down: function (queryInterface, Sequelize) {
16 | return queryInterface.dropTable('Temp')
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/string_x.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/views/error.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%- include('codimd/head') %>
6 |
7 |
8 |
9 |
10 | <%- include('codimd/header') %>
11 |
12 |
13 |
<%- code %> <%- detail %> <%- msg %>
14 |
15 |
16 | <%- include('codimd/footer') %>
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/test/testDoubles/otFake.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const sinon = require('sinon')
4 |
5 | class EditorSocketIOServerFake {
6 | constructor () {
7 | this.addClient = sinon.stub()
8 | this.onOperation = sinon.stub()
9 | this.onGetOperations = sinon.stub()
10 | this.updateSelection = sinon.stub()
11 | this.setName = sinon.stub()
12 | this.setColor = sinon.stub()
13 | this.getClient = sinon.stub()
14 | this.onDisconnect = sinon.stub()
15 | }
16 | }
17 |
18 | exports.EditorSocketIOServer = EditorSocketIOServerFake
19 |
--------------------------------------------------------------------------------
/public/js/lib/common/constant.ejs:
--------------------------------------------------------------------------------
1 | window.domain = '<%- domain %>'
2 | window.urlpath = '<%- urlpath %>'
3 | window.debug = <%- debug %>
4 | window.version = '<%- version %>'
5 | window.plantumlServer = '<%- plantumlServer %>'
6 |
7 | window.allowedUploadMimeTypes = <%- JSON.stringify(allowedUploadMimeTypes) %>
8 |
9 | window.defaultUseHardbreak = <%- defaultUseHardbreak %>
10 |
11 | window.linkifyHeaderStyle = '<%- linkifyHeaderStyle %>'
12 |
13 | window.DROPBOX_APP_KEY = '<%- DROPBOX_APP_KEY %>'
14 |
15 | window.USE_CDN = <%- useCDN %>
16 |
17 | window.defaultTocDepth = <%- defaultTocDepth %>
18 |
--------------------------------------------------------------------------------
/lib/middleware/redirectWithoutTrailingSlashes.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const config = require('../config')
4 |
5 | module.exports = function (req, res, next) {
6 | if (req.method === 'GET' && req.path.substr(-1) === '/' && req.path.length > 1) {
7 | const queryString = req.url.slice(req.path.length)
8 | const urlPath = req.path.slice(0, -1)
9 | let serverURL = config.serverURL
10 | if (config.urlPath) {
11 | serverURL = serverURL.slice(0, -(config.urlPath.length + 1))
12 | }
13 | res.redirect(301, serverURL + urlPath + queryString)
14 | } else {
15 | next()
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/lib/migrations/20180209120907-longtext-of-authorship.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | up: async function (queryInterface, Sequelize) {
5 | await queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT('long') })
6 | await queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT('long') })
7 | },
8 |
9 | down: async function (queryInterface, Sequelize) {
10 | await queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT })
11 | await queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT })
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/deployments/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -eo pipefail
4 | set -x
5 |
6 | if [[ -z $1 || -z $2 ]];then
7 | echo "build.sh [runtime image] [buildpack image]"
8 | exit 1
9 | fi
10 |
11 | CURRENT_DIR=$(dirname "$BASH_SOURCE")
12 |
13 | GIT_SHA1="$(git rev-parse HEAD)"
14 | GIT_SHORT_ID="${GIT_SHA1:0:8}"
15 | GIT_TAG=$(git describe --exact-match --tags $(git log -n1 --pretty='%h') 2>/dev/null || echo "")
16 |
17 | DOCKER_TAG="${GIT_TAG:-$GIT_SHORT_ID}"
18 |
19 | docker build --build-arg RUNTIME=$1 --build-arg BUILDPACK=$2 -t "hackmdio/hackmd:$DOCKER_TAG" -f "$CURRENT_DIR/Dockerfile" "$CURRENT_DIR/.."
20 |
21 |
--------------------------------------------------------------------------------
/lib/migrations/20150508114741-create-notes.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | up: function (queryInterface, Sequelize) {
4 | return queryInterface.createTable('Notes', {
5 | id: {
6 | type: Sequelize.UUID,
7 | primaryKey: true,
8 | defaultValue: Sequelize.UUIDV4
9 | },
10 | ownerId: Sequelize.UUID,
11 | content: Sequelize.TEXT,
12 | title: Sequelize.STRING,
13 | createdAt: Sequelize.DATE,
14 | updatedAt: Sequelize.DATE
15 | })
16 | },
17 |
18 | down: function (queryInterface, Sequelize) {
19 | return queryInterface.dropTable('Notes')
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/test/testDoubles/ProcessQueueFake.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | class ProcessQueueFake {
4 | constructor () {
5 | this.taskMap = new Map()
6 | this.queue = []
7 | }
8 |
9 | start () {
10 |
11 | }
12 |
13 | stop () {
14 |
15 | }
16 |
17 | checkTaskIsInQueue (id) {
18 | return this.taskMap.has(id)
19 | }
20 |
21 | push (id, processFunc) {
22 | this.queue.push({
23 | id: id,
24 | processFunc: processFunc
25 | })
26 | this.taskMap.set(id, true)
27 | }
28 |
29 | process () {
30 |
31 | }
32 | }
33 |
34 | exports.ProcessQueueFake = ProcessQueueFake
35 | exports.ProcessQueue = ProcessQueueFake
36 |
--------------------------------------------------------------------------------
/lib/errorPage/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const config = require('../config')
4 | const { responseError } = require('../response')
5 |
6 | exports.errorForbidden = (req, res) => {
7 | if (req.user) {
8 | return responseError(res, '403', 'Forbidden', 'oh no.')
9 | }
10 |
11 | req.flash('error', 'You are not allowed to access this page. Maybe try logging in?')
12 | res.redirect(config.serverURL + '/')
13 | }
14 |
15 | exports.errorNotFound = (req, res) => {
16 | responseError(res, '404', 'Not Found', 'oops.')
17 | }
18 |
19 | exports.errorInternalError = (req, res) => {
20 | responseError(res, '500', 'Internal Error', 'wtf.')
21 | }
22 |
--------------------------------------------------------------------------------
/webpack.htmlexport.js:
--------------------------------------------------------------------------------
1 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
2 | const path = require('path')
3 |
4 | module.exports = {
5 | name: 'save-as-html',
6 | entry: {
7 | htmlExport: path.join(__dirname, 'public/js/htmlExport.js')
8 | },
9 | module: {
10 | rules: [{
11 | test: /\.css$/,
12 | use: [MiniCssExtractPlugin.loader, 'css-loader']
13 | }]
14 | },
15 | output: {
16 | path: path.join(__dirname, 'public/build'),
17 | publicPath: '/build/',
18 | filename: '[name].js'
19 | },
20 | plugins: [
21 | new MiniCssExtractPlugin({
22 | filename: 'html.min.css'
23 | })
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/lib/logger.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const { createLogger, format, transports } = require('winston')
3 |
4 | const logger = createLogger({
5 | level: 'debug',
6 | format: format.combine(
7 | format.uncolorize(),
8 | format.timestamp(),
9 | format.align(),
10 | format.splat(),
11 | format.printf(info => `${info.timestamp} ${info.level}: ${info.message}`)
12 | ),
13 | transports: [
14 | new transports.Console({
15 | handleExceptions: true
16 | })
17 | ],
18 | exitOnError: false
19 | })
20 |
21 | logger.stream = {
22 | write: function (message, encoding) {
23 | logger.info(message)
24 | }
25 | }
26 |
27 | module.exports = logger
28 |
--------------------------------------------------------------------------------
/lib/migrations/20150504155329-create-users.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | up: function (queryInterface, Sequelize) {
4 | return queryInterface.createTable('Users', {
5 | id: {
6 | type: Sequelize.UUID,
7 | primaryKey: true,
8 | defaultValue: Sequelize.UUIDV4
9 | },
10 | profileid: {
11 | type: Sequelize.STRING,
12 | unique: true
13 | },
14 | profile: Sequelize.TEXT,
15 | history: Sequelize.TEXT,
16 | createdAt: Sequelize.DATE,
17 | updatedAt: Sequelize.DATE
18 | })
19 | },
20 |
21 | down: function (queryInterface, Sequelize) {
22 | return queryInterface.dropTable('Users')
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/webpack.prod.js:
--------------------------------------------------------------------------------
1 | const common = require('./webpack.common.js')
2 | const htmlexport = require('./webpack.htmlexport')
3 | const merge = require('webpack-merge')
4 | const path = require('path')
5 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
6 |
7 | module.exports = [
8 | merge(common, {
9 | mode: 'production',
10 | output: {
11 | path: path.join(__dirname, 'public/build'),
12 | publicPath: '/build/',
13 | filename: '[name].[contenthash].js'
14 | }
15 | }),
16 | merge(htmlexport, {
17 | mode: 'production',
18 | optimization: {
19 | minimizer: [
20 | new OptimizeCSSAssetsPlugin({})
21 | ]
22 | }
23 | })]
24 |
--------------------------------------------------------------------------------
/public/views/shared/disqus.ejs:
--------------------------------------------------------------------------------
1 |
2 |
13 |
19 |
--------------------------------------------------------------------------------
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | # [Choice] Node.js version: 16, 14
2 | ARG VARIANT=14-bullseye
3 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
4 |
5 | # [Optional] Uncomment this section to install additional OS packages.
6 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
7 | # && apt-get -y install --no-install-recommends
8 |
9 | # [Optional] Uncomment if you want to install an additional version of node using nvm
10 | # ARG EXTRA_NODE_VERSION=10
11 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
12 |
13 | # [Optional] Uncomment if you want to install more global node modules
14 | RUN su node -c "npm install -g npm@6"
15 |
--------------------------------------------------------------------------------
/lib/migrations/20161009040430-support-delete-note.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | up: function (queryInterface, Sequelize) {
4 | return queryInterface.addColumn('Notes', 'deletedAt', Sequelize.DATE).catch(function (error) {
5 | if (error.message === 'SQLITE_ERROR: duplicate column name: deletedAt' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'deletedAt'" || error.message === 'column "deletedAt" of relation "Notes" already exists') {
6 | console.log('Migration has already run… ignoring.')
7 | } else {
8 | throw error
9 | }
10 | })
11 | },
12 |
13 | down: function (queryInterface, Sequelize) {
14 | return queryInterface.removeColumn('Notes', 'deletedAt')
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/migrations/20180326103000-use-text-in-tokens.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | up: function (queryInterface, Sequelize) {
5 | return queryInterface.changeColumn('Users', 'accessToken', {
6 | type: Sequelize.TEXT
7 | }).then(function () {
8 | return queryInterface.changeColumn('Users', 'refreshToken', {
9 | type: Sequelize.TEXT
10 | })
11 | })
12 | },
13 |
14 | down: function (queryInterface, Sequelize) {
15 | return queryInterface.changeColumn('Users', 'accessToken', {
16 | type: Sequelize.STRING
17 | }).then(function () {
18 | return queryInterface.changeColumn('Users', 'refreshToken', {
19 | type: Sequelize.STRING
20 | })
21 | })
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/migrations/20200104215332-remove-temp-table.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | up: (queryInterface, Sequelize) => {
5 | return queryInterface.dropTable('Temp')
6 | /*
7 | Add altering commands here.
8 | Return a promise to correctly handle asynchronicity.
9 |
10 | Example:
11 | return queryInterface.createTable('users', { id: Sequelize.INTEGER });
12 | */
13 | },
14 |
15 | down: (queryInterface, Sequelize) => {
16 | return queryInterface.createTable('Temp', {
17 | id: {
18 | type: Sequelize.STRING,
19 | primaryKey: true
20 | },
21 | date: Sequelize.TEXT,
22 | createdAt: Sequelize.DATE,
23 | updatedAt: Sequelize.DATE
24 | })
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/public/views/shared/polyfill.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/public/js/lib/config/index.js:
--------------------------------------------------------------------------------
1 | export const DROPBOX_APP_KEY = window.DROPBOX_APP_KEY || ''
2 |
3 | export const domain = window.domain || '' // domain name
4 | export const urlpath = window.urlpath || '' // sub url path, like: www.example.com/
5 | export const debug = window.debug || false
6 |
7 | export const port = window.location.port
8 | export const serverurl = `${window.location.protocol}//${domain || window.location.hostname}${port ? ':' + port : ''}${urlpath ? '/' + urlpath : ''}`
9 | window.serverurl = serverurl
10 | export const noteid = decodeURIComponent(urlpath ? window.location.pathname.slice(urlpath.length + 1, window.location.pathname.length).split('/')[1] : window.location.pathname.split('/')[1])
11 | export const noteurl = `${serverurl}/${noteid}`
12 |
13 | export const version = window.version
14 |
--------------------------------------------------------------------------------
/public/css/site.css:
--------------------------------------------------------------------------------
1 | /* for all pages should include this */
2 | body {
3 | font-smoothing: subpixel-antialiased !important;
4 | -webkit-font-smoothing: subpixel-antialiased !important;
5 | -moz-osx-font-smoothing: auto !important;
6 | text-shadow: 0 0 1em transparent, 1px 1px 1.2px rgba(0, 0, 0, 0.004);
7 | /*text-rendering: optimizeLegibility;*/
8 | -webkit-overflow-scrolling: touch;
9 | font-family: "Source Sans Pro", Helvetica, Arial, sans-serif;
10 | letter-spacing: 0.025em;
11 | }
12 | :focus, .focus {
13 | outline: none !important;
14 | }
15 | ::-moz-focus-inner {
16 | border: 0 !important;
17 | }
18 |
19 | /* manual fix for bootstrap issue 14040, there is an unnecessary padding-right on modal open */
20 | body.modal-open {
21 | overflow-y: auto;
22 | padding-right: 0 !important;
23 | }
24 |
--------------------------------------------------------------------------------
/public/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // this config file is used in concert with the root .eslintrc.js in the root dir.
2 | module.exports = {
3 | "env": {
4 | "browser": true,
5 | "es6": true
6 | },
7 | "parserOptions": {
8 | "ecmaVersion": 2018,
9 | "sourceType": "module"
10 | },
11 | "globals": {
12 | "$": false,
13 | "CodeMirror": false,
14 | "Cookies": false,
15 | "moment": false,
16 | "editor": false,
17 | "ui": false,
18 | "modeType": false,
19 | "serverurl": false,
20 | "key": false,
21 | "gapi": false,
22 | "Dropbox": false,
23 | "FilePicker": false,
24 | "ot": false,
25 | "MediaUploader": false,
26 | "hex2rgb": false,
27 | "num_loaded": false,
28 | "Visibility": false,
29 | "inlineAttachment": false
30 | },
31 | "rules": {
32 | "no-unused-vars": "warn"
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/.mailmap:
--------------------------------------------------------------------------------
1 | Max Wu Wu Cheng-Han
2 | Max Wu Cheng-Han, Wu
3 | Max Wu jackycute
4 | Max Wu Wu, Cheng-Han
5 | Max Wu jackycute
6 |
7 | Sheogorath Christoph (Sheogorath) Kern
8 |
9 | Raccoon Raccoon Li
10 | Raccoon Raccoon
11 |
12 | Peter Dave Hello Peter Dave Hello
13 |
14 | Claudius Coenen Claudius Coenen
15 |
--------------------------------------------------------------------------------
/lib/migrations/20150915153700-change-notes-title-to-text.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const isSQLite = require('../utils').isSQLite
3 | module.exports = {
4 | up: function (queryInterface, Sequelize) {
5 | return queryInterface.changeColumn('Notes', 'title', {
6 | type: Sequelize.TEXT
7 | }).then(function () {
8 | if (isSQLite(queryInterface.sequelize)) {
9 | // manual added index will be removed in sqlite
10 | return queryInterface.addIndex('Notes', ['shortid'])
11 | }
12 | })
13 | },
14 |
15 | down: function (queryInterface, Sequelize) {
16 | return queryInterface.changeColumn('Notes', 'title', {
17 | type: Sequelize.STRING
18 | }).then(function () {
19 | if (isSQLite(queryInterface.sequelize)) {
20 | // manual added index will be removed in sqlite
21 | return queryInterface.addIndex('Notes', ['shortid'])
22 | }
23 | })
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/imageRouter/imgur.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const config = require('../config')
3 | const logger = require('../logger')
4 |
5 | const imgur = require('@hackmd/imgur')
6 |
7 | exports.uploadImage = function (imagePath, callback) {
8 | if (!imagePath || typeof imagePath !== 'string') {
9 | callback(new Error('Image path is missing or wrong'), null)
10 | return
11 | }
12 |
13 | if (!callback || typeof callback !== 'function') {
14 | logger.error('Callback has to be a function')
15 | return
16 | }
17 |
18 | imgur.setClientId(config.imgur.clientID)
19 | imgur.uploadFile(imagePath)
20 | .then(function (json) {
21 | if (config.debug) {
22 | logger.info('SERVER uploadimage success: ' + JSON.stringify(json))
23 | }
24 | callback(null, json.data.link.replace(/^http:\/\//i, 'https://'))
25 | }).catch(function (err) {
26 | callback(new Error(err), null)
27 | })
28 | }
29 |
--------------------------------------------------------------------------------
/lib/imageRouter/lutim.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const config = require('../config')
3 | const logger = require('../logger')
4 |
5 | const lutim = require('lutim')
6 |
7 | exports.uploadImage = function (imagePath, callback) {
8 | if (!imagePath || typeof imagePath !== 'string') {
9 | callback(new Error('Image path is missing or wrong'), null)
10 | return
11 | }
12 |
13 | if (!callback || typeof callback !== 'function') {
14 | logger.error('Callback has to be a function')
15 | return
16 | }
17 |
18 | if (config.lutim && config.lutim.url) {
19 | lutim.setAPIUrl(config.lutim.url)
20 | }
21 |
22 | lutim.uploadImage(imagePath)
23 | .then(function (json) {
24 | if (config.debug) {
25 | logger.info('SERVER uploadimage success: ' + JSON.stringify(json))
26 | }
27 | callback(null, lutim.getAPIUrl() + json.msg.short)
28 | }).catch(function (err) {
29 | callback(new Error(err), null)
30 | })
31 | }
32 |
--------------------------------------------------------------------------------
/lib/migrations/20160515114000-user-add-tokens.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | up: function (queryInterface, Sequelize) {
4 | return queryInterface.addColumn('Users', 'accessToken', Sequelize.STRING).then(function () {
5 | return queryInterface.addColumn('Users', 'refreshToken', Sequelize.STRING)
6 | }).catch(function (error) {
7 | if (error.message === 'SQLITE_ERROR: duplicate column name: accessToken' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'accessToken'" || error.message === 'column "accessToken" of relation "Users" already exists') {
8 | console.log('Migration has already run… ignoring.')
9 | } else {
10 | throw error
11 | }
12 | })
13 | },
14 |
15 | down: function (queryInterface, Sequelize) {
16 | return queryInterface.removeColumn('Users', 'accessToken').then(function () {
17 | return queryInterface.removeColumn('Users', 'refreshToken')
18 | })
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/lib/migrations/20171009121200-longtext-for-mysql.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | up: async function (queryInterface, Sequelize) {
4 | await queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT('long') })
5 | await queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT('long') })
6 | await queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT('long') })
7 | await queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT('long') })
8 | },
9 |
10 | down: async function (queryInterface, Sequelize) {
11 | await queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT })
12 | await queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT })
13 | await queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT })
14 | await queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT })
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/lib/migrations/20160420180355-note-add-alias.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | up: function (queryInterface, Sequelize) {
4 | return queryInterface.addColumn('Notes', 'alias', {
5 | type: Sequelize.STRING
6 | }).then(function () {
7 | return queryInterface.addIndex('Notes', ['alias'], {
8 | indicesType: 'UNIQUE'
9 | })
10 | }).catch(function (error) {
11 | if (error.message === 'SQLITE_ERROR: duplicate column name: alias' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'alias'" || error.message === 'column "alias" of relation "Notes" already exists') {
12 | console.log('Migration has already run… ignoring.')
13 | } else {
14 | throw error
15 | }
16 | })
17 | },
18 |
19 | down: function (queryInterface, Sequelize) {
20 | return queryInterface.removeColumn('Notes', 'alias').then(function () {
21 | return queryInterface.removeIndex('Notes', ['alias'])
22 | })
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/public/js/lib/editor/constants.js:
--------------------------------------------------------------------------------
1 | import { serverurl } from '../config'
2 |
3 | export const availableThemes = [
4 | { name: 'Light', value: 'default' },
5 | { name: 'One Dark (Default)', value: 'one-dark' },
6 | { name: 'Monokai', value: 'monokai' },
7 | { name: 'Solarized Dark', value: 'solarized dark' },
8 | { name: 'Solarized Light', value: 'solarized light' },
9 | { name: 'Dracula', value: 'dracula' },
10 | { name: 'Material', value: 'material' },
11 | { name: 'Nord', value: 'nord' },
12 | { name: 'Panda', value: 'panda-syntax' },
13 | { name: 'Ayu Dark', value: 'ayu-dark' },
14 | { name: 'Ayu Mirage', value: 'ayu-mirage' },
15 | { name: 'Tomorror Night Bright', value: 'tomorrow-night-bright' },
16 | { name: 'Tomorror Night Eighties', value: 'tomorrow-night-eighties' }
17 | ]
18 |
19 | export const emojifyImageDir = window.USE_CDN ? 'https://cdn.jsdelivr.net/npm/@hackmd/emojify.js@2.1.0/dist/images/basic' : `${serverurl}/build/emojify.js/dist/images/basic`
20 |
--------------------------------------------------------------------------------
/public/js/locale.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser, jquery */
2 | /* global Cookies */
3 |
4 | var lang = 'en'
5 | var userLang = navigator.language || navigator.userLanguage
6 | var userLangCode = userLang.split('-')[0]
7 | var locale = $('.ui-locale')
8 | var supportLangs = []
9 | $('.ui-locale option').each(function () {
10 | supportLangs.push($(this).val())
11 | })
12 | if (Cookies.get('locale')) {
13 | lang = Cookies.get('locale')
14 | if (lang === 'zh') {
15 | lang = 'zh-TW'
16 | }
17 | } else if (supportLangs.indexOf(userLang) !== -1) {
18 | lang = supportLangs[supportLangs.indexOf(userLang)]
19 | } else if (supportLangs.indexOf(userLangCode) !== -1) {
20 | lang = supportLangs[supportLangs.indexOf(userLangCode)]
21 | }
22 |
23 | locale.val(lang)
24 | $('select.ui-locale option[value="' + lang + '"]').attr('selected', 'selected')
25 |
26 | locale.change(function () {
27 | Cookies.set('locale', $(this).val(), {
28 | expires: 365
29 | })
30 | window.location.reload()
31 | })
32 |
--------------------------------------------------------------------------------
/lib/models/author.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | // external modules
3 | var Sequelize = require('sequelize')
4 |
5 | module.exports = function (sequelize, DataTypes) {
6 | var Author = sequelize.define('Author', {
7 | id: {
8 | type: Sequelize.INTEGER,
9 | primaryKey: true,
10 | autoIncrement: true
11 | },
12 | color: {
13 | type: DataTypes.STRING
14 | }
15 | }, {
16 | indexes: [
17 | {
18 | unique: true,
19 | fields: ['noteId', 'userId']
20 | }
21 | ]
22 | })
23 |
24 | Author.associate = function (models) {
25 | Author.belongsTo(models.Note, {
26 | foreignKey: 'noteId',
27 | as: 'note',
28 | constraints: false,
29 | onDelete: 'CASCADE',
30 | hooks: true
31 | })
32 | Author.belongsTo(models.User, {
33 | foreignKey: 'userId',
34 | as: 'user',
35 | constraints: false,
36 | onDelete: 'CASCADE',
37 | hooks: true
38 | })
39 | }
40 |
41 | return Author
42 | }
43 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | # run command at repo root
6 | CURRENT_PATH=$PWD
7 | if [ -d .git ]; then
8 | cd "$(git rev-parse --show-toplevel)"
9 | fi
10 |
11 | if ! type npm > /dev/null
12 | then
13 | cat << EOF
14 | npm is not installed, please install Node.js and npm.
15 | Read more on Node.js official website: https://nodejs.org
16 | Setup will not be run
17 | EOF
18 | exit 0
19 | fi
20 |
21 | echo "copy config files"
22 | if [ ! -f config.json ]; then
23 | cp config.json.example config.json
24 | fi
25 |
26 | if [ ! -f .sequelizerc ]; then
27 | cp .sequelizerc.example .sequelizerc
28 | fi
29 |
30 | echo "install packages"
31 | npm install
32 |
33 | cat << EOF
34 |
35 |
36 | Edit the following config file to setup CodiMD server and client.
37 | Read more info at https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-configuration
38 |
39 | * config.json -- CodiMD config
40 | * .sequelizerc -- db config
41 |
42 | EOF
43 |
44 | # change directory back
45 | cd "$CURRENT_PATH"
46 |
--------------------------------------------------------------------------------
/lib/auth/twitter/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Router = require('express').Router
4 | const passport = require('passport')
5 | const TwitterStrategy = require('passport-twitter').Strategy
6 |
7 | const config = require('../../config')
8 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
9 |
10 | const twitterAuth = module.exports = Router()
11 |
12 | passport.use(new TwitterStrategy({
13 | consumerKey: config.twitter.consumerKey,
14 | consumerSecret: config.twitter.consumerSecret,
15 | callbackURL: config.serverURL + '/auth/twitter/callback',
16 | state: true
17 | }, passportGeneralCallback))
18 |
19 | twitterAuth.get('/auth/twitter', function (req, res, next) {
20 | setReturnToFromReferer(req)
21 | passport.authenticate('twitter')(req, res, next)
22 | })
23 |
24 | // twitter auth callback
25 | twitterAuth.get('/auth/twitter/callback',
26 | passport.authenticate('twitter', {
27 | successReturnToOrRedirect: config.serverURL + '/',
28 | failureRedirect: config.serverURL + '/'
29 | })
30 | )
31 |
--------------------------------------------------------------------------------
/lib/auth/facebook/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Router = require('express').Router
4 | const passport = require('passport')
5 | const FacebookStrategy = require('passport-facebook').Strategy
6 |
7 | const config = require('../../config')
8 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
9 |
10 | const facebookAuth = module.exports = Router()
11 |
12 | passport.use(new FacebookStrategy({
13 | clientID: config.facebook.clientID,
14 | clientSecret: config.facebook.clientSecret,
15 | callbackURL: config.serverURL + '/auth/facebook/callback',
16 | state: true
17 | }, passportGeneralCallback))
18 |
19 | facebookAuth.get('/auth/facebook', function (req, res, next) {
20 | setReturnToFromReferer(req)
21 | passport.authenticate('facebook')(req, res, next)
22 | })
23 |
24 | // facebook auth callback
25 | facebookAuth.get('/auth/facebook/callback',
26 | passport.authenticate('facebook', {
27 | successReturnToOrRedirect: config.serverURL + '/',
28 | failureRedirect: config.serverURL + '/'
29 | })
30 | )
31 |
--------------------------------------------------------------------------------
/deployments/Dockerfile:
--------------------------------------------------------------------------------
1 | ARG RUNTIME
2 | ARG BUILDPACK
3 |
4 | FROM $BUILDPACK as BUILD
5 |
6 | COPY --chown=hackmd:hackmd . .
7 | ENV QT_QPA_PLATFORM=offscreen
8 |
9 | RUN set -xe && \
10 | git reset --hard && \
11 | git clean -fx && \
12 | npm install && \
13 | npm run build && \
14 | cp ./deployments/docker-entrypoint.sh ./ && \
15 | cp .sequelizerc.example .sequelizerc && \
16 | rm -rf .git .gitignore .travis.yml .dockerignore .editorconfig .babelrc .mailmap .sequelizerc.example \
17 | test docs contribute \
18 | package-lock.json webpack.prod.js webpack.htmlexport.js webpack.dev.js webpack.common.js \
19 | config.json.example README.md CONTRIBUTING.md AUTHORS node_modules
20 |
21 | FROM $RUNTIME
22 | USER hackmd
23 | ENV QT_QPA_PLATFORM=offscreen
24 | WORKDIR /home/hackmd/app
25 | COPY --chown=1500:1500 --from=BUILD /home/hackmd/app .
26 | RUN npm install --production && npm cache clean --force && rm -rf /tmp/{core-js-banners,phantomjs}
27 | EXPOSE 3000
28 | ENTRYPOINT ["/home/hackmd/app/docker-entrypoint.sh"]
29 |
--------------------------------------------------------------------------------
/lib/migrations/20160112220142-note-add-lastchange.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | up: function (queryInterface, Sequelize) {
4 | return queryInterface.addColumn('Notes', 'lastchangeuserId', {
5 | type: Sequelize.UUID
6 | }).then(function () {
7 | return queryInterface.addColumn('Notes', 'lastchangeAt', {
8 | type: Sequelize.DATE
9 | })
10 | }).catch(function (error) {
11 | if (error.message === 'SQLITE_ERROR: duplicate column name: lastchangeuserId' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'lastchangeuserId'" || error.message === 'column "lastchangeuserId" of relation "Notes" already exists') {
12 | console.log('Migration has already run… ignoring.')
13 | } else {
14 | throw error
15 | }
16 | })
17 | },
18 |
19 | down: function (queryInterface, Sequelize) {
20 | return queryInterface.removeColumn('Notes', 'lastchangeAt')
21 | .then(function () {
22 | return queryInterface.removeColumn('Notes', 'lastchangeuserId')
23 | })
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/auth/bitbucket/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Router = require('express').Router
4 | const passport = require('passport')
5 | const BitbucketStrategy = require('passport-bitbucket-oauth2').Strategy
6 | const config = require('../../config')
7 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
8 |
9 | const bitbucketAuth = module.exports = Router()
10 |
11 | passport.use(new BitbucketStrategy({
12 | clientID: config.bitbucket.clientID,
13 | clientSecret: config.bitbucket.clientSecret,
14 | callbackURL: config.serverURL + '/auth/bitbucket/callback',
15 | state: true
16 | }, passportGeneralCallback))
17 |
18 | bitbucketAuth.get('/auth/bitbucket', function (req, res, next) {
19 | setReturnToFromReferer(req)
20 | passport.authenticate('bitbucket')(req, res, next)
21 | })
22 |
23 | // bitbucket auth callback
24 | bitbucketAuth.get('/auth/bitbucket/callback',
25 | passport.authenticate('bitbucket', {
26 | successReturnToOrRedirect: config.serverURL + '/',
27 | failureRedirect: config.serverURL + '/'
28 | })
29 | )
30 |
--------------------------------------------------------------------------------
/lib/auth/dropbox/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Router = require('express').Router
4 | const passport = require('passport')
5 | const DropboxStrategy = require('passport-dropbox-oauth2').Strategy
6 | const config = require('../../config')
7 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
8 |
9 | const dropboxAuth = module.exports = Router()
10 |
11 | passport.use(new DropboxStrategy({
12 | apiVersion: '2',
13 | clientID: config.dropbox.clientID,
14 | clientSecret: config.dropbox.clientSecret,
15 | callbackURL: config.serverURL + '/auth/dropbox/callback',
16 | state: true
17 | }, passportGeneralCallback))
18 |
19 | dropboxAuth.get('/auth/dropbox', function (req, res, next) {
20 | setReturnToFromReferer(req)
21 | passport.authenticate('dropbox-oauth2')(req, res, next)
22 | })
23 |
24 | // dropbox auth callback
25 | dropboxAuth.get('/auth/dropbox/callback',
26 | passport.authenticate('dropbox-oauth2', {
27 | successReturnToOrRedirect: config.serverURL + '/',
28 | failureRedirect: config.serverURL + '/'
29 | })
30 | )
31 |
--------------------------------------------------------------------------------
/lib/homepage/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('fs')
4 | const path = require('path')
5 | const config = require('../config')
6 | const { User } = require('../models')
7 | const logger = require('../logger')
8 |
9 | exports.showIndex = async (req, res) => {
10 | const isLogin = req.isAuthenticated()
11 | const deleteToken = ''
12 |
13 | const data = {
14 | signin: isLogin,
15 | infoMessage: req.flash('info'),
16 | errorMessage: req.flash('error'),
17 | privacyStatement: fs.existsSync(path.join(config.docsPath, 'privacy.md')),
18 | termsOfUse: fs.existsSync(path.join(config.docsPath, 'terms-of-use.md')),
19 | deleteToken: deleteToken,
20 | csrfToken: req.csrfToken()
21 | }
22 |
23 | if (!isLogin) {
24 | return res.render('index.ejs', data)
25 | }
26 |
27 | const user = await User.findOne({
28 | where: {
29 | id: req.user.id
30 | }
31 | })
32 | if (user) {
33 | data.deleteToken = user.deleteToken
34 | return res.render('index.ejs', data)
35 | }
36 |
37 | logger.error(`error: user not found with id ${req.user.id}`)
38 | return res.render('index.ejs', data)
39 | }
40 |
--------------------------------------------------------------------------------
/lib/realtime/realtimeSaveRevisionJob.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const models = require('../models')
4 | const logger = require('../logger')
5 |
6 | /**
7 | * clean when user not in any rooms or user not in connected list
8 | */
9 | class SaveRevisionJob {
10 | constructor (realtime) {
11 | this.realtime = realtime
12 | this.saverSleep = false
13 | }
14 |
15 | start () {
16 | if (this.timer) return
17 | this.timer = setInterval(this.saveRevision.bind(this), 5 * 60 * 1000)
18 | }
19 |
20 | stop () {
21 | if (!this.timer) return
22 | clearInterval(this.timer)
23 | this.timer = undefined
24 | }
25 |
26 | saveRevision () {
27 | if (this.getSaverSleep()) return
28 | models.Revision.saveAllNotesRevision((err, notes) => {
29 | if (err) return logger.error('revision saver failed: ' + err)
30 | if (notes && notes.length <= 0) {
31 | this.setSaverSleep(true)
32 | }
33 | })
34 | }
35 |
36 | getSaverSleep () {
37 | return this.saverSleep
38 | }
39 |
40 | setSaverSleep (val) {
41 | this.saverSleep = val
42 | }
43 | }
44 |
45 | exports.SaveRevisionJob = SaveRevisionJob
46 |
--------------------------------------------------------------------------------
/lib/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const fs = require('fs')
3 | const path = require('path')
4 | const bodyParser = require('body-parser')
5 | const mime = require('mime-types')
6 |
7 | exports.isSQLite = function isSQLite (sequelize) {
8 | return sequelize.options.dialect === 'sqlite'
9 | }
10 |
11 | exports.getImageMimeType = function getImageMimeType (imagePath) {
12 | return mime.lookup(path.extname(imagePath))
13 | }
14 |
15 | exports.isRevealTheme = function isRevealTheme (theme) {
16 | if (fs.existsSync(path.join(__dirname, '..', 'public', 'build', 'reveal.js', 'css', 'theme', theme + '.css'))) {
17 | return theme
18 | }
19 | return undefined
20 | }
21 |
22 | exports.wrap = innerHandler => (req, res, next) => innerHandler(req, res).catch(err => next(err))
23 |
24 | // create application/x-www-form-urlencoded parser
25 | exports.urlencodedParser = bodyParser.urlencoded({
26 | extended: false,
27 | limit: 1024 * 1024 * 10 // 10 mb
28 | })
29 |
30 | // create text/markdown parser
31 | exports.markdownParser = bodyParser.text({
32 | inflate: true,
33 | type: ['text/plain', 'text/markdown'],
34 | limit: 1024 * 1024 * 10 // 10 mb
35 | })
36 |
--------------------------------------------------------------------------------
/deployments/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | database:
4 | image: postgres:11.6-alpine
5 | environment:
6 | - POSTGRES_USER=codimd
7 | - POSTGRES_PASSWORD=change_password
8 | - POSTGRES_DB=codimd
9 | volumes:
10 | - "database-data:/var/lib/postgresql/data"
11 | restart: always
12 | codimd:
13 | # you can use image or custom build below,
14 | image: nabo.codimd.dev/hackmdio/hackmd:2.5.3
15 | # Using the following command to trigger the build
16 | # docker-compose -f deployments/docker-compose.yml up --build
17 | # build:
18 | # context: ..
19 | # dockerfile: ./deployments/Dockerfile
20 | # args:
21 | # RUNTIME: hackmdio/runtime:16.20.2-35fe7e39
22 | # BUILDPACK: hackmdio/buildpack:16.20.2-35fe7e39
23 | environment:
24 | - CMD_DB_URL=postgres://codimd:change_password@database/codimd
25 | - CMD_USECDN=false
26 | depends_on:
27 | - database
28 | ports:
29 | - "3000:3000"
30 | volumes:
31 | - upload-data:/home/hackmd/app/public/uploads
32 | restart: always
33 | volumes:
34 | database-data: {}
35 | upload-data: {}
36 |
--------------------------------------------------------------------------------
/lib/auth/google/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Router = require('express').Router
4 | const passport = require('passport')
5 | var GoogleStrategy = require('passport-google-oauth20').Strategy
6 | const config = require('../../config')
7 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
8 |
9 | const googleAuth = module.exports = Router()
10 |
11 | passport.use(new GoogleStrategy({
12 | clientID: config.google.clientID,
13 | clientSecret: config.google.clientSecret,
14 | callbackURL: config.serverURL + '/auth/google/callback',
15 | userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo',
16 | state: true
17 | }, passportGeneralCallback))
18 |
19 | googleAuth.get('/auth/google', function (req, res, next) {
20 | setReturnToFromReferer(req)
21 | passport.authenticate('google', {
22 | scope: ['profile'],
23 | hostedDomain: config.google.hostedDomain
24 | })(req, res, next)
25 | })
26 | // google auth callback
27 | googleAuth.get('/auth/google/callback',
28 | passport.authenticate('google', {
29 | successReturnToOrRedirect: config.serverURL + '/',
30 | failureRedirect: config.serverURL + '/'
31 | })
32 | )
33 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/fretb_vert_4.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/auth/oauth2/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Router = require('express').Router
4 | const passport = require('passport')
5 |
6 | const config = require('../../config')
7 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
8 | const { OAuth2CustomStrategy } = require('./strategy')
9 |
10 | const oauth2Auth = module.exports = Router()
11 |
12 | passport.use(new OAuth2CustomStrategy({
13 | authorizationURL: config.oauth2.authorizationURL,
14 | tokenURL: config.oauth2.tokenURL,
15 | clientID: config.oauth2.clientID,
16 | clientSecret: config.oauth2.clientSecret,
17 | callbackURL: config.serverURL + '/auth/oauth2/callback',
18 | userProfileURL: config.oauth2.userProfileURL,
19 | state: config.oauth2.state,
20 | scope: config.oauth2.scope
21 | }, passportGeneralCallback))
22 |
23 | oauth2Auth.get('/auth/oauth2', function (req, res, next) {
24 | setReturnToFromReferer(req)
25 | passport.authenticate('oauth2')(req, res, next)
26 | })
27 |
28 | // github auth callback
29 | oauth2Auth.get('/auth/oauth2/callback',
30 | passport.authenticate('oauth2', {
31 | successReturnToOrRedirect: config.serverURL + '/',
32 | failureRedirect: config.serverURL + '/'
33 | })
34 | )
35 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/csvpreview.js:
--------------------------------------------------------------------------------
1 | import Papa from 'papaparse'
2 | import escapeHTML from 'lodash/escape'
3 |
4 | const safeParse = d => {
5 | try {
6 | return JSON.parse(d)
7 | } catch (err) {
8 | return d
9 | }
10 | }
11 |
12 | export function renderCSVPreview (csv, options = {}, attr = '') {
13 | const opt = Object.keys(options).reduce((acc, key) => {
14 | return Object.assign(acc, {
15 | [key]: safeParse(options[key])
16 | })
17 | }, {})
18 |
19 | const results = Papa.parse(csv.trim(), opt)
20 |
21 | if (opt.header) {
22 | const fields = results.meta.fields
23 | return `
24 |
25 |
26 | ${fields.map(f => `| ${escapeHTML(f)} | `).join('')}
27 |
28 |
29 |
30 | ${results.data.map(d => `
31 | ${fields.map(f => `| ${escapeHTML(d[f])} | `).join('')}
32 |
`).join('')}
33 |
34 |
`
35 | } else {
36 | return `
37 |
38 | ${results.data.map(d => `
39 | ${d.map(f => `| ${escapeHTML(f)} | `).join('')}
40 |
`).join('')}
41 |
42 |
`
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/migrations/20161201050312-support-email-signin.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | up: function (queryInterface, Sequelize) {
4 | return queryInterface.addColumn('Users', 'email', Sequelize.TEXT).then(function () {
5 | return queryInterface.addColumn('Users', 'password', Sequelize.TEXT).catch(function (error) {
6 | if (error.message === "ER_DUP_FIELDNAME: Duplicate column name 'password'" || error.message === 'column "password" of relation "Users" already exists') {
7 | console.log('Migration has already run… ignoring.')
8 | } else {
9 | throw error
10 | }
11 | })
12 | }).catch(function (error) {
13 | if (error.message === 'SQLITE_ERROR: duplicate column name: email' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'email'" || error.message === 'column "email" of relation "Users" already exists') {
14 | console.log('Migration has already run… ignoring.')
15 | } else {
16 | throw error
17 | }
18 | })
19 | },
20 |
21 | down: function (queryInterface, Sequelize) {
22 | return queryInterface.removeColumn('Users', 'email').then(function () {
23 | return queryInterface.removeColumn('Users', 'password')
24 | })
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/lib/imageRouter/azure.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const path = require('path')
3 |
4 | const config = require('../config')
5 | const logger = require('../logger')
6 |
7 | const azure = require('azure-storage')
8 |
9 | exports.uploadImage = function (imagePath, callback) {
10 | if (!imagePath || typeof imagePath !== 'string') {
11 | callback(new Error('Image path is missing or wrong'), null)
12 | return
13 | }
14 |
15 | if (!callback || typeof callback !== 'function') {
16 | logger.error('Callback has to be a function')
17 | return
18 | }
19 |
20 | var azureBlobService = azure.createBlobService(config.azure.connectionString)
21 |
22 | azureBlobService.createContainerIfNotExists(config.azure.container, { publicAccessLevel: 'blob' }, function (err, result, response) {
23 | if (err) {
24 | callback(new Error(err.message), null)
25 | } else {
26 | azureBlobService.createBlockBlobFromLocalFile(config.azure.container, path.basename(imagePath), imagePath, function (err, result, response) {
27 | if (err) {
28 | callback(new Error(err.message), null)
29 | } else {
30 | callback(null, azureBlobService.getUrl(config.azure.container, result.name))
31 | }
32 | })
33 | }
34 | })
35 | }
36 |
--------------------------------------------------------------------------------
/public/js/lib/common/metrics.ejs:
--------------------------------------------------------------------------------
1 | # HELP online_notes Number of currently used notes
2 | # TYPE online_notes gauge
3 | online_notes <%- onlineNotes %>
4 | # HELP online_users Number of online users
5 | # TYPE online_users gauge
6 | online_users <%- onlineUsers %>
7 | # HELP distinct_online_users Number of distinct online users
8 | # TYPE distinct_online_users gauge
9 | distinct_online_users <%- distinctOnlineUsers %>
10 | # HELP notes_count Total count of notes
11 | # TYPE notes_count gauge
12 | notes_count <%- notesCount %>
13 | # HELP registered_users Number of registered users
14 | # TYPE registered_users gauge
15 | registered_users <%- registeredUsers %>
16 | # HELP online_registered_users Number of online registered users
17 | # TYPE online_registered_users gauge
18 | online_registered_users <%- onlineRegisteredUsers %>
19 | # HELP distinct_online_registered_users Number of distinct online registered users
20 | # TYPE distinct_online_registered_users gauge
21 | distinct_online_registered_users <%- distinctOnlineRegisteredUsers %>
22 | is_connection_busy <%- isConnectionBusy ? 1 : 0 %>
23 | connection_socket_queue_length <%- connectionSocketQueueLength %>
24 | is_disconnect_busy <%- isDisconnectBusy ? 1: 0 %>
25 | disconnect_socket_queue_length <%- disconnectSocketQueueLength %>
26 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/fretb_vert_5.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/fretb_horiz_5.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/migrations/20160607060246-support-revision.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | up: function (queryInterface, Sequelize) {
4 | return queryInterface.addColumn('Notes', 'savedAt', Sequelize.DATE).then(function () {
5 | return queryInterface.createTable('Revisions', {
6 | id: {
7 | type: Sequelize.UUID,
8 | primaryKey: true
9 | },
10 | noteId: Sequelize.UUID,
11 | patch: Sequelize.TEXT,
12 | lastContent: Sequelize.TEXT,
13 | content: Sequelize.TEXT,
14 | length: Sequelize.INTEGER,
15 | createdAt: Sequelize.DATE,
16 | updatedAt: Sequelize.DATE
17 | })
18 | }).catch(function (error) {
19 | if (error.message === 'SQLITE_ERROR: duplicate column name: savedAt' | error.message === "ER_DUP_FIELDNAME: Duplicate column name 'savedAt'" || error.message === 'column "savedAt" of relation "Notes" already exists') {
20 | console.log('Migration has already run… ignoring.')
21 | } else {
22 | throw error
23 | }
24 | })
25 | },
26 |
27 | down: function (queryInterface, Sequelize) {
28 | return queryInterface.dropTable('Revisions').then(function () {
29 | return queryInterface.removeColumn('Notes', 'savedAt')
30 | })
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.github/workflows/check-release.yml:
--------------------------------------------------------------------------------
1 | name: Release PR Checks
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | branches:
7 | - master
8 |
9 | jobs:
10 | check-release-pr:
11 | if: startsWith(github.head_ref, 'release/')
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout code
16 | uses: actions/checkout@v4
17 | with:
18 | fetch-depth: 0
19 |
20 | - name: Check for release-notes updates
21 | run: |
22 | if ! git diff --exit-code origin/develop -- public/docs/release-notes.md; then
23 | echo "Release notes updated."
24 | else
25 | echo "Error: Release notes not updated in the PR."
26 | exit 1
27 | fi
28 |
29 | - name: Compare package.json version with master
30 | run: |
31 | git fetch origin master
32 | MASTER_PACKAGE_VERSION=$(git show origin/master:package.json | jq -r '.version')
33 | BRANCH_PACKAGE_VERSION=$(jq -r '.version' package.json)
34 |
35 | if [ "$BRANCH_PACKAGE_VERSION" != "$MASTER_PACKAGE_VERSION" ]; then
36 | echo "Version bumped in package.json."
37 | else
38 | echo "Error: Version in package.json has not been bumped."
39 | exit 1
40 | fi
41 |
42 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/fretb_horiz_6.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/css/slide-preview.css:
--------------------------------------------------------------------------------
1 | .markdown-body.slides {
2 | position: relative;
3 | z-index: 1;
4 | color: #222;
5 | }
6 |
7 | .markdown-body.slides::before {
8 | content: '';
9 | display: block;
10 | position: absolute;
11 | top: 0;
12 | left: 0;
13 | right: 0;
14 | bottom: 0;
15 | z-index: -1;
16 | background-color: currentColor;
17 | box-shadow: 0 0 0 50vw;
18 | }
19 |
20 | .markdown-body.slides section[data-markdown] {
21 | position: relative;
22 | margin-bottom: 1.5em;
23 | background-color: #fff;
24 | text-align: center;
25 | }
26 |
27 | .markdown-body.slides section[data-markdown] code {
28 | text-align: left;
29 | }
30 |
31 | .markdown-body.slides section[data-markdown]::before {
32 | content: '';
33 | display: block;
34 | padding-bottom: 56.23%;
35 | }
36 |
37 | .markdown-body.slides section[data-markdown] > div:first-child {
38 | position: absolute;
39 | top: 50%;
40 | left: 1em;
41 | right: 1em;
42 | transform: translateY(-50%);
43 | max-height: 100%;
44 | overflow: hidden;
45 | }
46 |
47 | .markdown-body.slides section[data-markdown] > ul {
48 | display: inline-block;
49 | }
50 |
51 | .markdown-body.slides > section > section + section::after {
52 | content: '';
53 | position: absolute;
54 | top: -1.5em;
55 | right: 1em;
56 | height: 1.5em;
57 | border: 3px solid #777;
58 | }
59 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/fretb_horiz_7.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/fretb_vert_7.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/migrations/20160703062241-support-authorship.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | up: function (queryInterface, Sequelize) {
4 | return queryInterface.addColumn('Notes', 'authorship', Sequelize.TEXT).then(function () {
5 | return queryInterface.addColumn('Revisions', 'authorship', Sequelize.TEXT)
6 | }).then(function () {
7 | return queryInterface.createTable('Authors', {
8 | id: {
9 | type: Sequelize.INTEGER,
10 | primaryKey: true,
11 | autoIncrement: true
12 | },
13 | color: Sequelize.STRING,
14 | noteId: Sequelize.UUID,
15 | userId: Sequelize.UUID,
16 | createdAt: Sequelize.DATE,
17 | updatedAt: Sequelize.DATE
18 | })
19 | }).catch(function (error) {
20 | if (error.message === 'SQLITE_ERROR: duplicate column name: authorship' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'authorship'" || error.message === 'column "authorship" of relation "Notes" already exists') {
21 | console.log('Migration has already run… ignoring.')
22 | } else {
23 | throw error
24 | }
25 | })
26 | },
27 |
28 | down: function (queryInterface, Sequelize) {
29 | return queryInterface.dropTable('Authors').then(function () {
30 | return queryInterface.removeColumn('Revisions', 'authorship')
31 | }).then(function () {
32 | return queryInterface.removeColumn('Notes', 'authorship')
33 | })
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/test/realtime/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const sinon = require('sinon')
4 | const path = require('path')
5 |
6 | function makeMockSocket (headers, query) {
7 | const broadCastChannelCache = {}
8 | return {
9 | id: Math.round(Math.random() * 10000),
10 | request: {
11 | user: {}
12 | },
13 | handshake: {
14 | headers: Object.assign({}, headers),
15 | query: Object.assign({}, query)
16 | },
17 | on: sinon.fake(),
18 | emit: sinon.fake(),
19 | broadCastChannelCache: {},
20 | broadcast: {
21 | to: (channel) => {
22 | if (!broadCastChannelCache[channel]) {
23 | broadCastChannelCache[channel] = {
24 | channel: channel,
25 | emit: sinon.fake()
26 | }
27 | }
28 | return broadCastChannelCache[channel]
29 | }
30 | },
31 | disconnect: sinon.fake(),
32 | rooms: []
33 | }
34 | }
35 |
36 | function removeModuleFromRequireCache (modulePath) {
37 | delete require.cache[require.resolve(modulePath)]
38 | }
39 | function removeLibModuleCache () {
40 | const libPath = path.resolve(path.join(__dirname, '../../lib'))
41 | Object.keys(require.cache).forEach(key => {
42 | if (key.startsWith(libPath)) {
43 | delete require.cache[require.resolve(key)]
44 | }
45 | })
46 | }
47 |
48 | exports.makeMockSocket = makeMockSocket
49 | exports.removeModuleFromRequireCache = removeModuleFromRequireCache
50 | exports.removeLibModuleCache = removeLibModuleCache
51 |
--------------------------------------------------------------------------------
/lib/realtime/realtimeCleanDanglingUserJob.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const async = require('async')
4 | const config = require('../config')
5 | const logger = require('../logger')
6 |
7 | /**
8 | * clean when user not in any rooms or user not in connected list
9 | */
10 | class CleanDanglingUserJob {
11 | constructor (realtime) {
12 | this.realtime = realtime
13 | }
14 |
15 | start () {
16 | if (this.timer) return
17 | this.timer = setInterval(this.cleanDanglingUser.bind(this), 60000)
18 | }
19 |
20 | stop () {
21 | if (!this.timer) return
22 | clearInterval(this.timer)
23 | this.timer = undefined
24 | }
25 |
26 | cleanDanglingUser () {
27 | const users = this.realtime.getUserPool()
28 | async.each(Object.keys(users), (key, callback) => {
29 | const socket = this.realtime.io.sockets.connected[key]
30 | if ((!socket && users[key]) ||
31 | (socket && (!socket.rooms || socket.rooms.length <= 0))) {
32 | if (config.debug) {
33 | logger.info('cleaner found redundant user: ' + key)
34 | }
35 | if (!socket) {
36 | return callback(null, null)
37 | }
38 | if (!this.realtime.disconnectProcessQueue.checkTaskIsInQueue(socket.id)) {
39 | this.realtime.queueForDisconnect(socket)
40 | }
41 | }
42 | return callback(null, null)
43 | }, function (err) {
44 | if (err) return logger.error('cleaner error', err)
45 | })
46 | }
47 | }
48 |
49 | exports.CleanDanglingUserJob = CleanDanglingUserJob
50 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/fretb_vert_9.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/contribute/developer-certificate-of-origin:
--------------------------------------------------------------------------------
1 | Developer Certificate of Origin
2 | Version 1.1
3 |
4 | Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
5 | 660 York Street, Suite 102,
6 | San Francisco, CA 94110 USA
7 |
8 | Everyone is permitted to copy and distribute verbatim copies of this
9 | license document, but changing it is not allowed.
10 |
11 | Developer's Certificate of Origin 1.1
12 |
13 | By making a contribution to this project, I certify that:
14 |
15 | (a) The contribution was created in whole or in part by me and I
16 | have the right to submit it under the open source license
17 | indicated in the file; or
18 |
19 | (b) The contribution is based upon previous work that, to the best
20 | of my knowledge, is covered under an appropriate open source
21 | license and I have the right under that license to submit that
22 | work with modifications, whether created in whole or in part
23 | by me, under the same open source license (unless I am
24 | permitted to submit under a different license), as indicated
25 | in the file; or
26 |
27 | (c) The contribution was provided directly to me by some other
28 | person who certified (a), (b) or (c) and I have not modified
29 | it.
30 |
31 | (d) I understand and agree that this project and the contribution
32 | are public and that a record of the contribution (including all
33 | personal information I submit with it, including my sign-off) is
34 | maintained indefinitely and may be redistributed consistent with
35 | this project or the open source license(s) involved.
36 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: 'Test and Build'
2 |
3 | on:
4 | push:
5 | pull_request:
6 | workflow_dispatch:
7 |
8 | jobs:
9 | test-and-build:
10 | runs-on: ubuntu-latest
11 | strategy:
12 | matrix:
13 | node-version: [16.x]
14 |
15 | steps:
16 | - uses: actions/checkout@v4
17 |
18 | # from https://stackoverflow.com/a/69649733
19 | - name: Reconfigure git to use HTTP authentication
20 | run: >
21 | git config --global url."https://github.com/".insteadOf
22 | ssh://git@github.com/
23 |
24 | - uses: actions/cache@v4
25 | with:
26 | path: ~/.npm
27 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
28 | restore-keys: |
29 | ${{ runner.os }}-node-
30 |
31 | - uses: actions/setup-node@v4
32 | name: Use Node.js ${{ matrix.node-version }}
33 | with:
34 | node-version: ${{ matrix.node-version }}
35 | check-latest: true
36 |
37 | - run: npm ci
38 | - run: npm run test:ci
39 | - run: npm run build
40 |
41 | doctoc:
42 | runs-on: ubuntu-latest
43 | if: github.ref == 'refs/heads/master' || github.event.pull_request
44 |
45 | steps:
46 | - uses: actions/checkout@v4
47 | - uses: actions/setup-node@v4
48 | name: Use Node.js 14
49 | with:
50 | node-version: 14
51 | check-latest: true
52 | - name: Install doctoc-check
53 | run: |
54 | npm install -g doctoc
55 | cp README.md README.md.orig
56 | npm run doctoc
57 | diff -q README.md README.md.orig
58 |
--------------------------------------------------------------------------------
/lib/auth/gitlab/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Router = require('express').Router
4 | const passport = require('passport')
5 | const GitlabStrategy = require('passport-gitlab2').Strategy
6 | const config = require('../../config')
7 | const response = require('../../response')
8 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
9 | const HttpsProxyAgent = require('https-proxy-agent')
10 |
11 | const gitlabAuth = module.exports = Router()
12 |
13 | const gitlabAuthStrategy = new GitlabStrategy({
14 | baseURL: config.gitlab.baseURL,
15 | clientID: config.gitlab.clientID,
16 | clientSecret: config.gitlab.clientSecret,
17 | scope: config.gitlab.scope,
18 | callbackURL: config.serverURL + '/auth/gitlab/callback',
19 | state: true
20 | }, passportGeneralCallback)
21 |
22 | if (process.env.https_proxy) {
23 | const httpsProxyAgent = new HttpsProxyAgent(process.env.https_proxy)
24 | gitlabAuthStrategy._oauth2.setAgent(httpsProxyAgent)
25 | }
26 |
27 | passport.use(gitlabAuthStrategy)
28 |
29 | gitlabAuth.get('/auth/gitlab', function (req, res, next) {
30 | setReturnToFromReferer(req)
31 | passport.authenticate('gitlab')(req, res, next)
32 | })
33 |
34 | // gitlab auth callback
35 | gitlabAuth.get('/auth/gitlab/callback',
36 | passport.authenticate('gitlab', {
37 | successReturnToOrRedirect: config.serverURL + '/',
38 | failureRedirect: config.serverURL + '/'
39 | })
40 | )
41 |
42 | if (!config.gitlab.scope || config.gitlab.scope === 'api') {
43 | // gitlab callback actions
44 | gitlabAuth.get('/auth/gitlab/callback/:noteId/:action', response.gitlabActions)
45 | }
46 |
--------------------------------------------------------------------------------
/.devcontainer/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | app:
5 | build:
6 | context: ..
7 | dockerfile: .devcontainer/Dockerfile
8 | args:
9 | VARIANT: 14-bullseye
10 | environment:
11 | - CMD_DB_URL=postgres://codimd:codimd@localhost/codimd
12 | - CMD_USECDN=false
13 | volumes:
14 | - ..:/workspace:cached
15 | - node_modules:/workspace/node_modules:cached
16 |
17 | # Overrides default command so things don't shut down after the process ends.
18 | command: sleep infinity
19 |
20 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
21 | network_mode: service:db
22 |
23 | # Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
24 |
25 | # Uncomment the next line to use a non-root user for all processes.
26 | # user: vscode
27 |
28 | # Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
29 | # (Adding the "ports" property to this file will not forward from a Codespace.)
30 |
31 | db:
32 | image: postgres:12.7-alpine
33 | restart: unless-stopped
34 | volumes:
35 | - postgres-data:/var/lib/postgresql/data
36 | environment:
37 | - POSTGRES_USER=codimd
38 | - POSTGRES_PASSWORD=codimd
39 | - POSTGRES_DB=codimd
40 |
41 | # Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
42 | # (Adding the "ports" property to this file will not forward from a Codespace.)
43 |
44 | volumes:
45 | node_modules:
46 | postgres-data:
--------------------------------------------------------------------------------
/lib/imageRouter/minio.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const fs = require('fs')
3 | const path = require('path')
4 |
5 | const config = require('../config')
6 | const { getImageMimeType } = require('../utils')
7 | const logger = require('../logger')
8 |
9 | const Minio = require('minio')
10 | const minioClient = new Minio.Client({
11 | endPoint: config.minio.endPoint,
12 | port: config.minio.port,
13 | useSSL: config.minio.secure,
14 | accessKey: config.minio.accessKey,
15 | secretKey: config.minio.secretKey
16 | })
17 |
18 | exports.uploadImage = function (imagePath, callback) {
19 | if (!imagePath || typeof imagePath !== 'string') {
20 | callback(new Error('Image path is missing or wrong'), null)
21 | return
22 | }
23 |
24 | if (!callback || typeof callback !== 'function') {
25 | logger.error('Callback has to be a function')
26 | return
27 | }
28 |
29 | fs.readFile(imagePath, function (err, buffer) {
30 | if (err) {
31 | callback(new Error(err), null)
32 | return
33 | }
34 |
35 | const key = path.join('uploads', path.basename(imagePath))
36 | const protocol = config.minio.secure ? 'https' : 'http'
37 |
38 | minioClient.putObject(config.s3bucket, key, buffer, buffer.size, getImageMimeType(imagePath), function (err, data) {
39 | if (err) {
40 | callback(new Error(err), null)
41 | return
42 | }
43 | const hidePort = [80, 443].includes(config.minio.port)
44 | const urlPort = hidePort ? '' : `:${config.minio.port}`
45 | callback(null, `${protocol}://${config.minio.endPoint}${urlPort}/${config.s3bucket}/${key}`)
46 | })
47 | })
48 | }
49 |
--------------------------------------------------------------------------------
/public/views/shared/revision-modal.ejs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/lib/migrations/20150702001020-update-to-0_3_1.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | module.exports = {
3 | up: function (queryInterface, Sequelize) {
4 | return queryInterface.addColumn('Notes', 'shortid', {
5 | type: Sequelize.STRING,
6 | defaultValue: '0000000000',
7 | allowNull: false
8 | }).then(function () {
9 | return queryInterface.addIndex('Notes', ['shortid'], {
10 | indicesType: 'UNIQUE'
11 | })
12 | }).then(function () {
13 | return queryInterface.addColumn('Notes', 'permission', {
14 | type: Sequelize.STRING,
15 | defaultValue: 'private',
16 | allowNull: false
17 | })
18 | }).then(function () {
19 | return queryInterface.addColumn('Notes', 'viewcount', {
20 | type: Sequelize.INTEGER,
21 | defaultValue: 0
22 | })
23 | }).catch(function (error) {
24 | if (error.message === 'SQLITE_ERROR: duplicate column name: shortid' || error.message === "ER_DUP_FIELDNAME: Duplicate column name 'shortid'" || error.message === 'column "shortid" of relation "Notes" already exists') {
25 | console.log('Migration has already run… ignoring.')
26 | } else {
27 | throw error
28 | }
29 | })
30 | },
31 |
32 | down: function (queryInterface, Sequelize) {
33 | return queryInterface.removeColumn('Notes', 'viewcount')
34 | .then(function () {
35 | return queryInterface.removeColumn('Notes', 'permission')
36 | })
37 | .then(function () {
38 | return queryInterface.removeIndex('Notes', ['shortid'])
39 | })
40 | .then(function () {
41 | return queryInterface.removeColumn('Notes', 'shortid')
42 | })
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/lib/auth/mattermost/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | require('babel-polyfill')
3 | require('isomorphic-fetch')
4 | const Router = require('express').Router
5 | const passport = require('passport')
6 | const { Client4: MattermostClient } = require('@mattermost/client')
7 | const OAuthStrategy = require('passport-oauth2').Strategy
8 | const config = require('../../config')
9 | const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
10 |
11 | const mattermostAuth = module.exports = Router()
12 |
13 | const mattermostClient = new MattermostClient()
14 |
15 | const mattermostStrategy = new OAuthStrategy({
16 | authorizationURL: config.mattermost.baseURL + '/oauth/authorize',
17 | tokenURL: config.mattermost.baseURL + '/oauth/access_token',
18 | clientID: config.mattermost.clientID,
19 | clientSecret: config.mattermost.clientSecret,
20 | callbackURL: config.serverURL + '/auth/mattermost/callback'
21 | }, passportGeneralCallback)
22 |
23 | mattermostStrategy.userProfile = (accessToken, done) => {
24 | mattermostClient.setUrl(config.mattermost.baseURL)
25 | mattermostClient.setToken(accessToken)
26 | mattermostClient.getMe()
27 | .then((data) => done(null, data))
28 | .catch((err) => done(err))
29 | }
30 |
31 | passport.use(mattermostStrategy)
32 |
33 | mattermostAuth.get('/auth/mattermost', function (req, res, next) {
34 | setReturnToFromReferer(req)
35 | passport.authenticate('oauth2')(req, res, next)
36 | })
37 |
38 | // mattermost auth callback
39 | mattermostAuth.get('/auth/mattermost/callback',
40 | passport.authenticate('oauth2', {
41 | successReturnToOrRedirect: config.serverURL + '/',
42 | failureRedirect: config.serverURL + '/'
43 | })
44 | )
45 |
--------------------------------------------------------------------------------
/public/views/index/foot.ejs:
--------------------------------------------------------------------------------
1 | <% if(useCDN) { %>
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | <%- include('../build/cover-scripts') %>
11 | <% } else { %>
12 | <%- include('../build/cover-pack-scripts') %>
13 | <% } %>
14 |
--------------------------------------------------------------------------------
/lib/status/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const realtime = require('../realtime/realtime')
4 | const config = require('../config')
5 |
6 | exports.getStatus = async (req, res) => {
7 | res.set({
8 | 'Cache-Control': 'private', // only cache by client
9 | 'X-Robots-Tag': 'noindex, nofollow', // prevent crawling
10 | 'Content-Type': 'application/json'
11 | })
12 |
13 | try {
14 | const data = await realtime.getStatus()
15 | res.send(data)
16 | } catch (e) {
17 | console.error(e)
18 | res.status(500).send(e.toString())
19 | }
20 | }
21 |
22 | exports.getMetrics = async (req, res) => {
23 | const data = await realtime.getStatus()
24 |
25 | res.set({
26 | 'Cache-Control': 'private', // only cache by client
27 | 'X-Robots-Tag': 'noindex, nofollow', // prevent crawling
28 | 'Content-Type': 'text/plain; charset=utf-8'
29 | })
30 | res.render('../js/lib/common/metrics.ejs', data)
31 | }
32 |
33 | exports.getConfig = (req, res) => {
34 | const data = {
35 | domain: config.domain,
36 | urlpath: config.urlPath,
37 | debug: config.debug,
38 | version: config.fullversion,
39 | plantumlServer: config.plantuml.server,
40 | DROPBOX_APP_KEY: config.dropbox.appKey,
41 | allowedUploadMimeTypes: config.allowedUploadMimeTypes,
42 | defaultUseHardbreak: config.defaultUseHardbreak,
43 | linkifyHeaderStyle: config.linkifyHeaderStyle,
44 | useCDN: config.useCDN,
45 | defaultTocDepth: config.defaultTocDepth
46 | }
47 | res.set({
48 | 'Cache-Control': 'private', // only cache by client
49 | 'X-Robots-Tag': 'noindex, nofollow', // prevent crawling
50 | 'Content-Type': 'application/javascript'
51 | })
52 | res.render('../js/lib/common/constant.ejs', data)
53 | }
54 |
--------------------------------------------------------------------------------
/lib/letter-avatars.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | // external modules
3 | const crypto = require('crypto')
4 | const randomcolor = require('randomcolor')
5 | const config = require('./config')
6 |
7 | // core
8 | exports.generateAvatar = function (name) {
9 | const color = randomcolor({
10 | seed: name,
11 | luminosity: 'dark'
12 | })
13 | const letter = name.substring(0, 1).toUpperCase()
14 |
15 | let svg = ''
16 | svg += ''
24 |
25 | return svg
26 | }
27 |
28 | exports.generateAvatarURL = function (name, email = '', big = true) {
29 | let photo
30 | if (typeof email !== 'string') {
31 | email = '' + name + '@example.com'
32 | }
33 | name = encodeURIComponent(name)
34 |
35 | const hash = crypto.createHash('md5')
36 | hash.update(email.toLowerCase())
37 | const hexDigest = hash.digest('hex')
38 |
39 | if (email !== '' && config.allowGravatar) {
40 | photo = 'https://www.gravatar.com/avatar/' + hexDigest
41 | if (big) {
42 | photo += '?s=400'
43 | } else {
44 | photo += '?s=96'
45 | }
46 | } else {
47 | photo = config.serverURL + '/user/' + (name || email.substring(0, email.lastIndexOf('@')) || hexDigest) + '/avatar.svg'
48 | }
49 | return photo
50 | }
51 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/fretb_vert_12.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/models/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | // external modules
3 | var fs = require('fs')
4 | var path = require('path')
5 | var Sequelize = require('sequelize')
6 | const { cloneDeep } = require('lodash')
7 |
8 | // core
9 | var config = require('../config')
10 | var logger = require('../logger')
11 |
12 | var dbconfig = cloneDeep(config.db)
13 | dbconfig.logging = config.debug ? (data) => {
14 | logger.info(data)
15 | } : false
16 |
17 | var sequelize = null
18 |
19 | // Heroku specific
20 | if (config.dbURL) {
21 | sequelize = new Sequelize(config.dbURL, dbconfig)
22 | } else {
23 | sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig)
24 | }
25 |
26 | // [Postgres] Handling NULL bytes
27 | // https://github.com/sequelize/sequelize/issues/6485
28 | function stripNullByte (value) {
29 | value = '' + value
30 | // eslint-disable-next-line no-control-regex
31 | return value ? value.replace(/\u0000/g, '') : value
32 | }
33 | sequelize.stripNullByte = stripNullByte
34 |
35 | function processData (data, _default, process) {
36 | if (data === undefined) return data
37 | else return data === null ? _default : (process ? process(data) : data)
38 | }
39 | sequelize.processData = processData
40 |
41 | var db = {}
42 |
43 | fs.readdirSync(__dirname)
44 | .filter(function (file) {
45 | return (file.indexOf('.') !== 0) && (file !== 'index.js')
46 | })
47 | .forEach(function (file) {
48 | var model = require(path.join(__dirname, file))(sequelize, Sequelize)
49 | db[model.name] = model
50 | })
51 |
52 | Object.keys(db).forEach(function (modelName) {
53 | if ('associate' in db[modelName]) {
54 | db[modelName].associate(db)
55 | }
56 | })
57 |
58 | db.sequelize = sequelize
59 | db.Sequelize = Sequelize
60 |
61 | module.exports = db
62 |
--------------------------------------------------------------------------------
/public/views/shared/refresh-modal.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
<%= __('You have an incompatible client version.') %>
13 | <%= __('Refresh to update.') %>
14 |
15 |
21 |
22 |
<%= __('Your user state has changed.') %>
23 | <%= __('Refresh to load new user state.') %>
24 |
25 |
26 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/public/images/mattermost-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
34 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "CodiMD",
3 | "dockerComposeFile": "docker-compose.yml",
4 | "service": "app",
5 | "workspaceFolder": "/workspace",
6 |
7 | // Set *default* container specific settings.json values on container create.
8 | "settings": {
9 | "terminal.integrated.shell.linux": "/bin/zsh",
10 | "sqltools.connections": [{
11 | "name": "Container Database",
12 | "driver": "PostgreSQL",
13 | "previewLimit": 50,
14 | "server": "localhost",
15 | "port": 5432,
16 | "database": "codimd",
17 | "username": "codimd",
18 | "password": "codimd"
19 | }],
20 | },
21 |
22 | // Add the IDs of extensions you want installed when the container is created.
23 | "extensions": [
24 | "dbaeumer.vscode-eslint",
25 | "visualstudioexptteam.vscodeintellicode",
26 | "christian-kohler.path-intellisense",
27 | "standard.vscode-standard",
28 | "mtxr.sqltools",
29 | "mtxr.sqltools-driver-pg",
30 | "eamodio.gitlens",
31 | "codestream.codestream",
32 | "github.vscode-pull-request-github",
33 | "cschleiden.vscode-github-actions",
34 | "hbenl.vscode-mocha-test-adapter",
35 | "hbenl.vscode-test-explorer"
36 | ],
37 |
38 | // Use 'forwardPorts' to make a list of ports inside the container available locally.
39 | // "forwardPorts": [],
40 |
41 | "portsAttributes": {
42 | "3000": {
43 | "label": "CodiMD server",
44 | "onAutoForward": "notify"
45 | },
46 | "5432": {
47 | "label": "PostgreSQL",
48 | "onAutoForward": "notify"
49 | }
50 | },
51 |
52 | // Use 'postCreateCommand' to run commands after the container is created.
53 | // "postCreateCommand": "yarn install",
54 | "postCreateCommand": "sudo chown -R node:node node_modules && /workspace/bin/setup",
55 |
56 | // Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
57 | "remoteUser": "node"
58 | }
--------------------------------------------------------------------------------
/lib/ot/server.js:
--------------------------------------------------------------------------------
1 | var config = require('../config');
2 |
3 | if (typeof ot === 'undefined') {
4 | var ot = {};
5 | }
6 |
7 | ot.Server = (function (global) {
8 | 'use strict';
9 |
10 | // Constructor. Takes the current document as a string and optionally the array
11 | // of all operations.
12 | function Server (document, operations) {
13 | this.document = document;
14 | this.operations = operations || [];
15 | }
16 |
17 | // Call this method whenever you receive an operation from a client.
18 | Server.prototype.receiveOperation = function (revision, operation) {
19 | if (revision < 0 || this.operations.length < revision) {
20 | throw new Error("operation revision not in history");
21 | }
22 | // Find all operations that the client didn't know of when it sent the
23 | // operation ...
24 | var concurrentOperations = this.operations.slice(revision);
25 |
26 | // ... and transform the operation against all these operations ...
27 | var transform = operation.constructor.transform;
28 | for (var i = 0; i < concurrentOperations.length; i++) {
29 | operation = transform(operation, concurrentOperations[i])[0];
30 | }
31 |
32 | // ... and apply that on the document.
33 | var newDocument = operation.apply(this.document);
34 | // ignore if exceed the max length of document
35 | if(newDocument.length > config.documentMaxLength && newDocument.length > this.document.length)
36 | return;
37 | this.document = newDocument;
38 | // Store operation in history.
39 | this.operations.push(operation);
40 |
41 | // It's the caller's responsibility to send the operation to all connected
42 | // clients and an acknowledgement to the creator.
43 | return operation;
44 | };
45 |
46 | return Server;
47 |
48 | }(this));
49 |
50 | if (typeof module === 'object') {
51 | module.exports = ot.Server;
52 | }
--------------------------------------------------------------------------------
/public/js/lib/markdown/utils.js:
--------------------------------------------------------------------------------
1 | export function parseFenceCodeParams (lang) {
2 | const attrMatch = lang.match(/{(.*)}/)
3 | const params = {}
4 | if (attrMatch && attrMatch.length >= 2) {
5 | const attrs = attrMatch[1]
6 | const paraMatch = attrs.match(/([#.](\S+?)\s)|((\S+?)\s*=\s*("(.+?)"|'(.+?)'|\[[^\]]*\]|\{[}]*\}|(\S+)))/g)
7 | paraMatch && paraMatch.forEach(param => {
8 | param = param.trim()
9 | if (param[0] === '#') {
10 | params.id = param.slice(1)
11 | } else if (param[0] === '.') {
12 | if (!params.class) params.class = []
13 | params.class = params.class.concat(param.slice(1))
14 | } else {
15 | const offset = param.indexOf('=')
16 | const id = param.substring(0, offset).trim().toLowerCase()
17 | let val = param.substring(offset + 1).trim()
18 | const valStart = val[0]
19 | const valEnd = val[val.length - 1]
20 | if (['"', "'"].indexOf(valStart) !== -1 && ['"', "'"].indexOf(valEnd) !== -1 && valStart === valEnd) {
21 | val = val.substring(1, val.length - 1)
22 | }
23 | if (id === 'class') {
24 | if (params.class) params.class = []
25 | params.class = params.class.concat(val)
26 | } else {
27 | params[id] = val
28 | }
29 | }
30 | })
31 | }
32 | return params
33 | }
34 |
35 | export function serializeParamToAttribute (params) {
36 | if (Object.getOwnPropertyNames(params).length === 0) {
37 | return ''
38 | } else {
39 | return ` data-params="${escape(JSON.stringify(params))}"`
40 | }
41 | }
42 |
43 | /**
44 | * @param {HTMLElement} elem
45 | */
46 | export function deserializeParamAttributeFromElement (elem) {
47 | const params = elem.getAttribute('data-params')
48 | if (params) {
49 | return JSON.parse(unescape(params))
50 | } else {
51 | return {}
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/lib/imageRouter/filesystem.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const crypto = require('crypto')
4 | const fs = require('fs')
5 | const URL = require('url').URL
6 | const path = require('path')
7 |
8 | const config = require('../config')
9 | const logger = require('../logger')
10 |
11 | /**
12 | * generate a random filename for uploaded image
13 | */
14 | function randomFilename () {
15 | const buf = crypto.randomBytes(16)
16 | return `upload_${buf.toString('hex')}`
17 | }
18 |
19 | /**
20 | * pick a filename not exist in filesystem
21 | * maximum attempt 5 times
22 | */
23 | function pickFilename (defaultFilename) {
24 | let retryCounter = 5
25 | const extname = path.extname(defaultFilename)
26 | let filename = `${randomFilename()}${extname}`
27 | while (retryCounter-- > 0) {
28 | if (fs.existsSync(path.join(config.uploadsPath, filename))) {
29 | filename = `${randomFilename()}${extname}`
30 | continue
31 | }
32 | return filename
33 | }
34 | throw new Error('file exists.')
35 | }
36 |
37 | exports.uploadImage = function (imagePath, callback) {
38 | if (!imagePath || typeof imagePath !== 'string') {
39 | callback(new Error('Image path is missing or wrong'), null)
40 | return
41 | }
42 |
43 | if (!callback || typeof callback !== 'function') {
44 | logger.error('Callback has to be a function')
45 | return
46 | }
47 |
48 | let filename = path.basename(imagePath)
49 | try {
50 | filename = pickFilename(path.basename(imagePath))
51 | } catch (e) {
52 | return callback(e, null)
53 | }
54 |
55 | try {
56 | fs.copyFileSync(imagePath, path.join(config.uploadsPath, filename))
57 | } catch (e) {
58 | return callback(e, null)
59 | }
60 |
61 | let url
62 | try {
63 | url = (new URL(filename, config.serverURL + '/uploads/')).href
64 | } catch (e) {
65 | url = config.serverURL + '/uploads/' + filename
66 | }
67 |
68 | callback(null, url)
69 | }
70 |
--------------------------------------------------------------------------------
/lib/config/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('fs')
4 | const path = require('path')
5 |
6 | exports.toBooleanConfig = function toBooleanConfig (configValue) {
7 | if (configValue && typeof configValue === 'string') {
8 | return (configValue === 'true')
9 | }
10 | return configValue
11 | }
12 |
13 | exports.toArrayConfig = function toArrayConfig (configValue, separator = ',', fallback) {
14 | if (configValue && typeof configValue === 'string') {
15 | return (configValue.split(separator).map(arrayItem => arrayItem.trim()))
16 | }
17 | return fallback
18 | }
19 |
20 | exports.toIntegerConfig = function toIntegerConfig (configValue) {
21 | if (configValue && typeof configValue === 'string') {
22 | return parseInt(configValue)
23 | }
24 | return configValue
25 | }
26 |
27 | exports.getGitCommit = function getGitCommit (repodir) {
28 | if (!fs.existsSync(repodir + '/.git/HEAD')) {
29 | return undefined
30 | }
31 | let reference = fs.readFileSync(repodir + '/.git/HEAD', 'utf8')
32 | if (reference.startsWith('ref: ')) {
33 | reference = reference.substr(5).replace('\n', '')
34 | reference = fs.readFileSync(path.resolve(repodir + '/.git', reference), 'utf8')
35 | }
36 | reference = reference.replace('\n', '')
37 | return reference
38 | }
39 |
40 | exports.getGitHubURL = function getGitHubURL (repo, reference) {
41 | // if it's not a github reference, we handle handle that anyway
42 | if (!repo.startsWith('https://github.com') && !repo.startsWith('git@github.com')) {
43 | return repo
44 | }
45 | if (repo.startsWith('git@github.com') || repo.startsWith('ssh://git@github.com')) {
46 | repo = repo.replace(/^(ssh:\/\/)?git@github.com:/, 'https://github.com/')
47 | }
48 |
49 | if (repo.endsWith('.git')) {
50 | repo = repo.replace(/\.git$/, '/')
51 | } else if (!repo.endsWith('/')) {
52 | repo = repo + '/'
53 | }
54 | return repo + 'tree/' + reference
55 | }
56 |
--------------------------------------------------------------------------------
/public/css/codemirror-extend/tomorrow-night-bright.css:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Name: Tomorrow Night - Bright
4 | Author: Chris Kempson
5 |
6 | Port done by Gerard Braad
7 |
8 | */
9 |
10 | .cm-s-tomorrow-night-bright.CodeMirror { background: #000000; color: #eaeaea; }
11 | .cm-s-tomorrow-night-bright div.CodeMirror-selected { background: #424242; }
12 | .cm-s-tomorrow-night-bright .CodeMirror-gutters { background: #000000; border-right: 0px; }
13 | .cm-s-tomorrow-night-bright .CodeMirror-guttermarker { color: #e78c45; }
14 | .cm-s-tomorrow-night-bright .CodeMirror-guttermarker-subtle { color: #777; }
15 | .cm-s-tomorrow-night-bright .CodeMirror-linenumber { color: #424242; }
16 | .cm-s-tomorrow-night-bright .CodeMirror-cursor { border-left: 1px solid #6A6A6A; }
17 |
18 | .cm-s-tomorrow-night-bright span.cm-comment { color: #d27b53; }
19 | .cm-s-tomorrow-night-bright span.cm-atom { color: #a16a94; }
20 | .cm-s-tomorrow-night-bright span.cm-number { color: #a16a94; }
21 |
22 | .cm-s-tomorrow-night-bright span.cm-property, .cm-s-tomorrow-night-bright span.cm-attribute { color: #99cc99; }
23 | .cm-s-tomorrow-night-bright span.cm-keyword { color: #d54e53; }
24 | .cm-s-tomorrow-night-bright span.cm-string { color: #e7c547; }
25 |
26 | .cm-s-tomorrow-night-bright span.cm-variable { color: #b9ca4a; }
27 | .cm-s-tomorrow-night-bright span.cm-variable-2 { color: #7aa6da; }
28 | .cm-s-tomorrow-night-bright span.cm-def { color: #e78c45; }
29 | .cm-s-tomorrow-night-bright span.cm-bracket { color: #eaeaea; }
30 | .cm-s-tomorrow-night-bright span.cm-tag { color: #d54e53; }
31 | .cm-s-tomorrow-night-bright span.cm-link { color: #a16a94; }
32 | .cm-s-tomorrow-night-bright span.cm-error { background: #d54e53; color: #6A6A6A; }
33 |
34 | .cm-s-tomorrow-night-bright .CodeMirror-activeline-background { background: #2a2a2a; }
35 | .cm-s-tomorrow-night-bright .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }
36 |
--------------------------------------------------------------------------------
/lib/web/middleware/checkVersion.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const { promisify } = require('util')
4 |
5 | const request = require('request')
6 |
7 | const logger = require('../../logger')
8 | const config = require('../../config')
9 |
10 | let lastCheckAt
11 |
12 | const VERSION_CHECK_ENDPOINT = 'https://evangelion.codimd.dev/'
13 | const CHECK_TIMEOUT = 1000 * 60 * 60 * 24 // 1 day
14 |
15 | const rp = promisify(request)
16 |
17 | exports.checkVersion = checkVersion
18 | /**
19 | * @param {Express.Application|Express.Request} ctx
20 | */
21 | async function checkVersion (ctx) {
22 | if (lastCheckAt && (lastCheckAt + CHECK_TIMEOUT > Date.now())) {
23 | return
24 | }
25 |
26 | // update lastCheckAt whether the check would fail or not
27 | lastCheckAt = Date.now()
28 |
29 | try {
30 | const { statusCode, body: data } = await rp({
31 | url: `${VERSION_CHECK_ENDPOINT}?v=${config.version}`,
32 | method: 'GET',
33 | json: true,
34 | timeout: 3000
35 | })
36 |
37 | if (statusCode !== 200 || data.status === 'error') {
38 | logger.warn('Version check failed.')
39 | return
40 | }
41 |
42 | const locals = ctx.locals ? ctx.locals : ctx.app.locals
43 |
44 | locals.versionInfo.latest = data.latest
45 | locals.versionInfo.versionItem = data.latest ? null : data.versionItem
46 |
47 | if (!data.latest) {
48 | const { version, link } = data.versionItem
49 |
50 | logger.info(`Your CodiMD version is out of date! The latest version is ${version}. Please see what's new on ${link}.`)
51 | }
52 | } catch (err) {
53 | // ignore and skip version check
54 | logger.warn('Version check failed.')
55 | logger.warn(err)
56 | }
57 | }
58 |
59 | exports.versionCheckMiddleware = function (req, res, next) {
60 | checkVersion(req)
61 | .then(() => {
62 | next()
63 | })
64 | .catch((err) => {
65 | logger.error(err)
66 | next()
67 | })
68 | }
69 |
--------------------------------------------------------------------------------
/public/js/utils.js:
--------------------------------------------------------------------------------
1 | /* global fetch */
2 | import base64url from 'base64url'
3 |
4 | const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
5 |
6 | export function checkNoteIdValid (id) {
7 | const result = id.match(uuidRegex)
8 | return !!(result && result.length === 1)
9 | }
10 |
11 | export function encodeNoteId (id) {
12 | // remove dashes in UUID and encode in url-safe base64
13 | const str = id.replace(/-/g, '')
14 | const hexStr = Buffer.from(str, 'hex')
15 | return base64url.encode(hexStr)
16 | }
17 |
18 | export function decodeNoteId (encodedId) {
19 | // decode from url-safe base64
20 | const id = base64url.toBuffer(encodedId).toString('hex')
21 | // add dashes between the UUID string parts
22 | const idParts = []
23 | idParts.push(id.substr(0, 8))
24 | idParts.push(id.substr(8, 4))
25 | idParts.push(id.substr(12, 4))
26 | idParts.push(id.substr(16, 4))
27 | idParts.push(id.substr(20, 12))
28 | return idParts.join('-')
29 | }
30 |
31 | /**
32 | * sanitize url to prevent XSS
33 | * @see {@link https://github.com/braintree/sanitize-url/issues/52#issue-1593777166}
34 | *
35 | * @param {string} rawUrl
36 | * @returns {string} sanitized url
37 | */
38 | export function sanitizeUrl (rawUrl) {
39 | try {
40 | const url = new URL(rawUrl)
41 | if (url.protocol === 'http:' || url.protocol === 'https:') {
42 | return url.toString()
43 | }
44 |
45 | throw new Error('Invalid protocol')
46 | } catch (error) {
47 | return 'about:blank'
48 | }
49 | }
50 |
51 | // Check if URL is a PDF based on Content-Type header
52 | export async function isPdfUrl (url) {
53 | try {
54 | const response = await fetch(url, { method: 'HEAD' })
55 | const contentType = response.headers.get('Content-Type')
56 | return contentType === 'application/pdf'
57 | } catch (error) {
58 | console.warn('Error checking PDF content type:', error)
59 | return false
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/fretb_vert_15.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/views/shared/pandoc-export-modal.ejs:
--------------------------------------------------------------------------------
1 |
2 |
34 |
--------------------------------------------------------------------------------
/public/docs/privacy.md.example:
--------------------------------------------------------------------------------
1 | Privacy
2 | ===
3 |
4 | We process the following data, for the following purposes:
5 |
6 | |your data|our usage|
7 | |---------|---------|
8 | |IP-Address|Used to communicate with your browser and our servers. It's may exposed to third-parties which provide resources for this service. These services are, depending on your login method, the document you visit and the setup of this instance: Google, Disqus, MathJax, GitHub, SlideShare/LinkedIn, yahoo, Gravatar, Imgur, Amazon, and Cloudflare.|
9 | |Usernames and profiles|Your username as well as user profiles that are connected with it are transmitted and stored by us to provide a useful login integration with services like GitHub, Facebook, Twitter, GitLab, Dropbox, Google. Depending on the setup of this CodiMD instance there are maybe other third-parties involved using SAML, LDAP or the integration with a Mattermost instance.|
10 | |Profile pictures| Your profile picture is either loaded from the service you used to login, the CodiMD instance or Gravatar.|
11 | |Uploaded pictures| Pictures that are uploaded for documents are either uploaded to Amazon S3, Imgur, a minio instance or the local filesystem of the CodiMD server.|
12 |
13 | All account data and notes are stored in a mysql/postgres/sqlite database. Besides the user accounts and the document themselves also relationships between the documents and the user accounts are stored. This includes ownership, authorship and revisions of all changes made during the creation of a note.
14 |
15 | To delete your account and all your notes owned by your user account, you can find a button in the drop down menu on the front page.
16 |
17 | The deletion of guest notes is not possible. These don't have any ownership and this means we can't connect these to you or anyone else. If you participated in a guest note or a note owned by someone else, your authorship for the revisions is removed from these notes as well. But the content you created will stay in place as the integrity of these notes has to stay untouched.
18 |
--------------------------------------------------------------------------------
/lib/auth/openid/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Router = require('express').Router
4 | const passport = require('passport')
5 | const OpenIDStrategy = require('@passport-next/passport-openid').Strategy
6 | const config = require('../../config')
7 | const models = require('../../models')
8 | const logger = require('../../logger')
9 | const { urlencodedParser } = require('../../utils')
10 | const { setReturnToFromReferer } = require('../utils')
11 |
12 | const openIDAuth = module.exports = Router()
13 |
14 | passport.use(new OpenIDStrategy({
15 | returnURL: config.serverURL + '/auth/openid/callback',
16 | realm: config.serverURL,
17 | profile: true
18 | }, function (openid, profile, done) {
19 | var stringifiedProfile = JSON.stringify(profile)
20 | models.User.findOrCreate({
21 | where: {
22 | profileid: openid
23 | },
24 | defaults: {
25 | profile: stringifiedProfile
26 | }
27 | }).spread(function (user, created) {
28 | if (user) {
29 | var needSave = false
30 | if (user.profile !== stringifiedProfile) {
31 | user.profile = stringifiedProfile
32 | needSave = true
33 | }
34 | if (needSave) {
35 | user.save().then(function () {
36 | if (config.debug) { logger.info('user login: ' + user.id) }
37 | return done(null, user)
38 | })
39 | } else {
40 | if (config.debug) { logger.info('user login: ' + user.id) }
41 | return done(null, user)
42 | }
43 | }
44 | }).catch(function (err) {
45 | logger.error('auth callback failed: ' + err)
46 | return done(err, null)
47 | })
48 | }))
49 |
50 | openIDAuth.post('/auth/openid', urlencodedParser, function (req, res, next) {
51 | setReturnToFromReferer(req)
52 | passport.authenticate('openid')(req, res, next)
53 | })
54 |
55 | // openID auth callback
56 | openIDAuth.get('/auth/openid/callback',
57 | passport.authenticate('openid', {
58 | successReturnToOrRedirect: config.serverURL + '/',
59 | failureRedirect: config.serverURL + '/'
60 | })
61 | )
62 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/number7.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/number7_h.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/auth/oauth2/strategy.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node, mocha */
2 | 'use strict'
3 |
4 | const assert = require('assert')
5 | const chance = require('chance')()
6 |
7 | const { extractProfileAttribute } = require('../../../lib/auth/oauth2/strategy')
8 |
9 | describe('OAuth2CustomStrategy', function () {
10 | describe('#extractProfileAttribute', function () {
11 | const data = {
12 | user: {
13 | email: chance.email()
14 | },
15 | arrayData: [
16 | {
17 | email: chance.email()
18 | },
19 | {
20 | email: chance.email()
21 | }
22 | ]
23 | }
24 |
25 | it('should parse normal attribute correctly', function () {
26 | assert(extractProfileAttribute(data, 'user.email') === data.user.email)
27 | })
28 |
29 | it('should return undefined when nested object key not exists', function () {
30 | assert(extractProfileAttribute(data, 'user.profile') === undefined)
31 | })
32 |
33 | it('should return undefined when whole object key not exists', function () {
34 | assert(extractProfileAttribute(data, 'profile.email') === undefined)
35 | })
36 |
37 | it('should return attribute in array correct', function () {
38 | assert(extractProfileAttribute(data, 'arrayData[0].email') === data.arrayData[0].email)
39 | assert(extractProfileAttribute(data, 'arrayData[1].email') === data.arrayData[1].email)
40 | })
41 |
42 | it('should return undefined when array index out of bound', function () {
43 | assert(extractProfileAttribute(data, 'arrayData[3].email') === undefined)
44 | })
45 |
46 | it('should return undefined when array key not exists', function () {
47 | assert(extractProfileAttribute(data, 'notExistsArray[5].email') === undefined)
48 | })
49 |
50 | it('should return undefined when data is undefined', function () {
51 | assert(extractProfileAttribute(undefined, 'email') === undefined)
52 | assert(extractProfileAttribute(null, 'email') === undefined)
53 | assert(extractProfileAttribute({}, 'email') === undefined)
54 | })
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/public/views/index/head.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | CodiMD - <%= __('Collaborative markdown notes') %>
10 |
11 |
12 | <% if(useCDN) { %>
13 |
14 |
15 |
16 |
17 |
18 | <%- include('../build/cover-header') %>
19 | <%- include('../shared/polyfill') %>
20 | <% } else { %>
21 |
22 |
23 | <%- include('../build/cover-pack-header') %>
24 | <% } %>
25 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/number1.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/number1_h.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/lib/auth/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Router = require('express').Router
4 | const passport = require('passport')
5 |
6 | const config = require('../config')
7 | const logger = require('../logger')
8 | const models = require('../models')
9 |
10 | const authRouter = module.exports = Router()
11 |
12 | // serialize and deserialize
13 | passport.serializeUser(function (user, done) {
14 | logger.info('serializeUser: ' + user.id)
15 | return done(null, user.id)
16 | })
17 |
18 | passport.deserializeUser(function (id, done) {
19 | models.User.findOne({
20 | where: {
21 | id: id
22 | }
23 | }).then(function (user) {
24 | // Don't die on non-existent user
25 | if (user == null) {
26 | return done(null, false, { message: 'Invalid UserID' })
27 | }
28 |
29 | logger.info('deserializeUser: ' + user.id)
30 | return done(null, user)
31 | }).catch(function (err) {
32 | logger.error(err)
33 | return done(err, null)
34 | })
35 | })
36 |
37 | if (config.isFacebookEnable) authRouter.use(require('./facebook'))
38 | if (config.isTwitterEnable) authRouter.use(require('./twitter'))
39 | if (config.isGitHubEnable) authRouter.use(require('./github'))
40 | if (config.isBitbucketEnable) authRouter.use(require('./bitbucket'))
41 | if (config.isGitLabEnable) authRouter.use(require('./gitlab'))
42 | if (config.isMattermostEnable) authRouter.use(require('./mattermost'))
43 | if (config.isDropboxEnable) authRouter.use(require('./dropbox'))
44 | if (config.isGoogleEnable) authRouter.use(require('./google'))
45 | if (config.isLDAPEnable) authRouter.use(require('./ldap'))
46 | if (config.isSAMLEnable) authRouter.use(require('./saml'))
47 | if (config.isOAuth2Enable) authRouter.use(require('./oauth2'))
48 | if (config.isEmailEnable) authRouter.use(require('./email'))
49 | if (config.isOpenIDEnable) authRouter.use(require('./openid'))
50 |
51 | // logout
52 | authRouter.get('/logout', function (req, res, next) {
53 | if (config.debug && req.isAuthenticated()) {
54 | logger.debug('user logout: ' + req.user.id)
55 | }
56 |
57 | req.logout((err) => {
58 | if (err) { return next(err) }
59 |
60 | res.redirect(config.serverURL + '/')
61 | })
62 | })
63 |
--------------------------------------------------------------------------------
/lib/imageRouter/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const fs = require('fs')
4 | const path = require('path')
5 | const Router = require('express').Router
6 | const formidable = require('formidable')
7 |
8 | const readChunk = require('read-chunk')
9 | const imageType = require('image-type')
10 | const mime = require('mime-types')
11 |
12 | const config = require('../config')
13 | const logger = require('../logger')
14 | const response = require('../response')
15 |
16 | const imageRouter = module.exports = Router()
17 |
18 | function checkImageValid (filepath) {
19 | try {
20 | const buffer = readChunk.sync(filepath, 0, 12)
21 | /** @type {{ ext: string, mime: string } | null} */
22 | const mimetypeFromBuf = imageType(buffer)
23 | const mimeTypeFromExt = mime.lookup(path.extname(filepath))
24 |
25 | return mimetypeFromBuf && config.allowedUploadMimeTypes.includes(mimetypeFromBuf.mime) &&
26 | mimeTypeFromExt && config.allowedUploadMimeTypes.includes(mimeTypeFromExt)
27 | } catch (err) {
28 | logger.error(err)
29 | return false
30 | }
31 | }
32 |
33 | // upload image
34 | imageRouter.post('/uploadimage', function (req, res) {
35 | var form = new formidable.IncomingForm({
36 | keepExtensions: true
37 | })
38 |
39 | form.parse(req, function (err, fields, files) {
40 | if (err || !files.image || !files.image.filepath) {
41 | response.errorForbidden(req, res)
42 | } else {
43 | if (config.debug) {
44 | logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image))
45 | }
46 |
47 | if (!checkImageValid(files.image.filepath)) {
48 | return response.errorForbidden(req, res)
49 | }
50 |
51 | const uploadProvider = require('./' + config.imageUploadType)
52 | uploadProvider.uploadImage(files.image.filepath, function (err, url) {
53 | // remove temporary upload file, and ignore any error
54 | fs.unlink(files.image.filepath, () => {})
55 | if (err !== null) {
56 | logger.error(err)
57 | return res.status(500).end('upload image error')
58 | }
59 | res.send({
60 | link: url
61 | })
62 | })
63 | }
64 | })
65 | })
66 |
--------------------------------------------------------------------------------
/test/realtime/cleanDanglingUser.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node, mocha */
2 | 'use strict'
3 |
4 | const assert = require('assert')
5 | const mock = require('mock-require')
6 | const sinon = require('sinon')
7 | const { removeModuleFromRequireCache, makeMockSocket } = require('./utils')
8 |
9 | describe('cleanDanglingUser', function () {
10 | let clock
11 | beforeEach(() => {
12 | clock = sinon.useFakeTimers()
13 | mock('../../lib/processQueue', require('../testDoubles/ProcessQueueFake'))
14 | mock('../../lib/logger', {
15 | error: () => {},
16 | info: () => {}
17 | })
18 | mock('../../lib/history', {})
19 | mock('../../lib/models', {
20 | Revision: {
21 | saveAllNotesRevision: () => {
22 | }
23 | }
24 | })
25 | mock('../../lib/config', {
26 | debug: true
27 | })
28 | mock('../../lib/realtimeUpdateDirtyNoteJob', require('../testDoubles/realtimeJobStub'))
29 | mock('../../lib/realtimeSaveRevisionJob', require('../testDoubles/realtimeJobStub'))
30 | })
31 |
32 | afterEach(() => {
33 | clock.restore()
34 | removeModuleFromRequireCache('../../lib/realtime/realtime')
35 | mock.stopAll()
36 | sinon.restore()
37 | })
38 |
39 | it('should call queueForDisconnectSpy when user is dangling', (done) => {
40 | const realtime = require('../../lib/realtime/realtime')
41 | const queueForDisconnectSpy = sinon.spy(realtime, 'queueForDisconnect')
42 | realtime.io = {
43 | to: sinon.stub().callsFake(function () {
44 | return {
45 | emit: sinon.fake()
46 | }
47 | }),
48 | sockets: {
49 | connected: {}
50 | }
51 | }
52 | const user1Socket = makeMockSocket()
53 | const user2Socket = makeMockSocket()
54 |
55 | user1Socket.rooms.push('room1')
56 |
57 | realtime.io.sockets.connected[user1Socket.id] = user1Socket
58 | realtime.io.sockets.connected[user2Socket.id] = user2Socket
59 |
60 | realtime.users[user1Socket.id] = user1Socket
61 | realtime.users[user2Socket.id] = user2Socket
62 | clock.tick(60000)
63 | clock.restore()
64 | setTimeout(() => {
65 | assert(queueForDisconnectSpy.called)
66 | done()
67 | }, 50)
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/public/markdown-lint/css/lint.css:
--------------------------------------------------------------------------------
1 | /* The lint marker gutter */
2 | .CodeMirror-lint-markers {
3 | width: 16px;
4 | }
5 |
6 | .CodeMirror-lint-tooltip {
7 | background-color: #333333;
8 | border: 1px solid #eeeeee;
9 | border-radius: 4px;
10 | color: white;
11 | font-family: "Source Code Pro", Consolas, monaco, monospace;
12 | font-size: 10pt;
13 | overflow: hidden;
14 | padding: 2px 5px;
15 | position: fixed;
16 | white-space: pre;
17 | white-space: pre-wrap;
18 | z-index: 100;
19 | max-width: 600px;
20 | opacity: 0;
21 | transition: opacity .4s;
22 | -moz-transition: opacity .4s;
23 | -webkit-transition: opacity .4s;
24 | -o-transition: opacity .4s;
25 | -ms-transition: opacity .4s;
26 | }
27 |
28 | .CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning {
29 | background-position: left bottom;
30 | background-repeat: repeat-x;
31 | }
32 |
33 | .CodeMirror-lint-mark-error {
34 | background-image: url(../images/mark-error.png);
35 | }
36 |
37 | .CodeMirror-lint-mark-warning {
38 | background-image: url(../images/mark-warning.png);
39 | }
40 |
41 | .CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning {
42 | background-position: center center;
43 | background-repeat: no-repeat;
44 | cursor: pointer;
45 | display: inline-block;
46 | height: 16px;
47 | width: 16px;
48 | vertical-align: middle;
49 | position: relative;
50 | margin-left: 5px;
51 | }
52 |
53 | .CodeMirror-lint-message-error, .CodeMirror-lint-message-warning {
54 | padding-left: 20px;
55 | background-position: top left;
56 | background-repeat: no-repeat;
57 | background-position-y: 2px;
58 | }
59 |
60 | .CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
61 | background-image: url(../images/message-error.png);
62 | }
63 |
64 | .CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
65 | background-image: url(../images/message-warning.png);
66 | }
67 |
68 | .CodeMirror-lint-marker-multiple {
69 | background-image: url(../images/mark-multiple.png);
70 | background-repeat: no-repeat;
71 | background-position: right bottom;
72 | width: 100%; height: 100%;
73 | }
74 |
75 | .CodeMirror-hints {
76 | background: #333;
77 | }
78 |
79 | .CodeMirror-hint {
80 | color: white;
81 | }
82 |
--------------------------------------------------------------------------------
/test/realtime/realtime.test.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /* eslint-env node, mocha */
4 |
5 | const mock = require('mock-require')
6 | const assert = require('assert')
7 |
8 | describe('realtime', function () {
9 | describe('checkViewPermission', function () {
10 | // role -> guest, loggedInUser, loggedInOwner
11 | const viewPermission = {
12 | freely: [true, true, true],
13 | editable: [true, true, true],
14 | limited: [false, true, true],
15 | locked: [true, true, true],
16 | protected: [false, true, true],
17 | private: [false, false, true]
18 | }
19 | const loggedInUserId = 'user1_id'
20 | const ownerUserId = 'user2_id'
21 | const guestReq = {}
22 | const loggedInUserReq = {
23 | user: {
24 | id: loggedInUserId,
25 | logged_in: true
26 | }
27 | }
28 | const loggedInOwnerReq = {
29 | user: {
30 | id: ownerUserId,
31 | logged_in: true
32 | }
33 | }
34 |
35 | const note = {
36 | owner: ownerUserId
37 | }
38 |
39 | let realtime
40 |
41 | beforeEach(() => {
42 | mock('../../lib/logger', {
43 | error: () => {
44 | }
45 | })
46 | mock('../../lib/history', {})
47 | mock('../../lib/models', {
48 | Note: {
49 | parseNoteTitle: (data) => (data)
50 | }
51 | })
52 | mock('../../lib/config', {})
53 | realtime = require('../../lib/realtime/realtime')
54 | })
55 |
56 | Object.keys(viewPermission).forEach(function (permission) {
57 | describe(permission, function () {
58 | beforeEach(() => {
59 | note.permission = permission
60 | })
61 | it('guest permission test', function () {
62 | assert(realtime.checkViewPermission(guestReq, note) === viewPermission[permission][0])
63 | })
64 | it('loggedIn User permission test', function () {
65 | assert(realtime.checkViewPermission(loggedInUserReq, note) === viewPermission[permission][1])
66 | })
67 | it('loggedIn Owner permission test', function () {
68 | assert(realtime.checkViewPermission(loggedInOwnerReq, note) === viewPermission[permission][2])
69 | })
70 | })
71 | })
72 | })
73 | })
74 |
--------------------------------------------------------------------------------
/test/letter-avatars.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node, mocha */
2 |
3 | 'use strict'
4 |
5 | const assert = require('assert')
6 | const mock = require('mock-require')
7 |
8 | describe('generateAvatarURL() gravatar enabled', function () {
9 | let avatars
10 | beforeEach(function () {
11 | // Reset config to make sure we don't influence other tests
12 | const testconfig = {
13 | allowGravatar: true,
14 | serverURL: 'http://localhost:3000',
15 | port: 3000
16 | }
17 | mock('../lib/config', testconfig)
18 | avatars = mock.reRequire('../lib/letter-avatars')
19 | })
20 |
21 | it('should return correct urls', function () {
22 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', true), 'https://www.gravatar.com/avatar/d41b5f3508cc3f31865566a47dd0336b?s=400')
23 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', false), 'https://www.gravatar.com/avatar/d41b5f3508cc3f31865566a47dd0336b?s=96')
24 | })
25 |
26 | it('should return correct urls for names with spaces', function () {
27 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels'), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg')
28 | })
29 | })
30 |
31 | describe('generateAvatarURL() gravatar disabled', function () {
32 | let avatars
33 | beforeEach(function () {
34 | // Reset config to make sure we don't influence other tests
35 | const testconfig = {
36 | allowGravatar: false,
37 | serverURL: 'http://localhost:3000',
38 | port: 3000
39 | }
40 | mock('../lib/config', testconfig)
41 | avatars = mock.reRequire('../lib/letter-avatars')
42 | })
43 |
44 | it('should return correct urls', function () {
45 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', true), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg')
46 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels', 'hello@dsprenkels.com', false), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg')
47 | })
48 |
49 | it('should return correct urls for names with spaces', function () {
50 | assert.strictEqual(avatars.generateAvatarURL('Daan Sprenkels'), 'http://localhost:3000/user/Daan%20Sprenkels/avatar.svg')
51 | })
52 | })
53 |
--------------------------------------------------------------------------------
/lib/imageRouter/s3.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 | const fs = require('fs')
3 | const path = require('path')
4 |
5 | const config = require('../config')
6 | const { getImageMimeType } = require('../utils')
7 | const logger = require('../logger')
8 |
9 | const { S3Client } = require('@aws-sdk/client-s3-node/S3Client')
10 | const { PutObjectCommand } = require('@aws-sdk/client-s3-node/commands/PutObjectCommand')
11 |
12 | const credentials = {
13 | accessKeyId: config.s3.accessKeyId,
14 | secretAccessKey: config.s3.secretAccessKey
15 | }
16 |
17 | const s3 = new S3Client({
18 | credentials,
19 | region: config.s3.region,
20 | endpoint: config.s3.endpoint
21 | })
22 |
23 | exports.uploadImage = function (imagePath, callback) {
24 | if (!imagePath || typeof imagePath !== 'string') {
25 | callback(new Error('Image path is missing or wrong'), null)
26 | return
27 | }
28 |
29 | if (!callback || typeof callback !== 'function') {
30 | logger.error('Callback has to be a function')
31 | return
32 | }
33 |
34 | fs.readFile(imagePath, function (err, buffer) {
35 | if (err) {
36 | callback(new Error(err), null)
37 | return
38 | }
39 | const params = {
40 | Bucket: config.s3bucket,
41 | Key: path.join('uploads', path.basename(imagePath)),
42 | Body: buffer,
43 | ACL: 'public-read'
44 | }
45 | const mimeType = getImageMimeType(imagePath)
46 | if (mimeType) { params.ContentType = mimeType }
47 |
48 | const command = new PutObjectCommand(params)
49 |
50 | s3.send(command).then(data => {
51 | // default scheme settings to https
52 | let s3Endpoint = 'https://s3.amazonaws.com'
53 | if (config.s3.region && config.s3.region !== 'us-east-1') {
54 | s3Endpoint = `https://s3-${config.s3.region}.amazonaws.com`
55 | }
56 | // rewrite endpoint from config
57 | if (config.s3.endpoint) {
58 | s3Endpoint = config.s3.endpoint
59 | }
60 | if (config.s3.baseURL) {
61 | callback(null, `${config.s3.baseURL}/${params.Key}`)
62 | } else {
63 | callback(null, `${s3Endpoint}/${config.s3bucket}/${params.Key}`)
64 | }
65 | }).catch(err => {
66 | if (err) {
67 | callback(new Error(err), null)
68 | }
69 | })
70 | })
71 | }
72 |
--------------------------------------------------------------------------------
/public/vendor/ot/socketio-adapter.js:
--------------------------------------------------------------------------------
1 | /*global ot */
2 |
3 | ot.SocketIOAdapter = (function () {
4 | 'use strict';
5 |
6 | function SocketIOAdapter(socket) {
7 | this.socket = socket;
8 |
9 | var self = this;
10 | socket.on('client_left', function (clientId) {
11 | self.trigger('client_left', clientId);
12 | });
13 | socket.on('set_name', function (clientId, name) {
14 | self.trigger('set_name', clientId, name);
15 | });
16 | socket.on('set_color', function (clientId, color) {
17 | self.trigger('set_color', clientId, color);
18 | });
19 | socket.on('ack', function (revision) {
20 | self.trigger('ack', revision);
21 | });
22 | socket.on('operation', function (clientId, revision, operation, selection) {
23 | self.trigger('operation', revision, operation);
24 | self.trigger('selection', clientId, selection);
25 | });
26 | socket.on('operations', function (head, operations) {
27 | self.trigger('operations', head, operations);
28 | });
29 | socket.on('selection', function (clientId, selection) {
30 | self.trigger('selection', clientId, selection);
31 | });
32 | socket.on('reconnect', function () {
33 | self.trigger('reconnect');
34 | });
35 | }
36 |
37 | SocketIOAdapter.prototype.sendOperation = function (revision, operation, selection) {
38 | this.socket.emit('operation', revision, operation, selection);
39 | };
40 |
41 | SocketIOAdapter.prototype.sendSelection = function (selection) {
42 | this.socket.emit('selection', selection);
43 | };
44 |
45 | SocketIOAdapter.prototype.getOperations = function (base, head) {
46 | this.socket.emit('get_operations', base, head);
47 | };
48 |
49 | SocketIOAdapter.prototype.registerCallbacks = function (cb) {
50 | this.callbacks = cb;
51 | };
52 |
53 | SocketIOAdapter.prototype.trigger = function (event) {
54 | var args = Array.prototype.slice.call(arguments, 1);
55 | var action = this.callbacks && this.callbacks[event];
56 | if (action) {
57 | action.apply(this, args);
58 | }
59 | };
60 |
61 | return SocketIOAdapter;
62 |
63 | }());
--------------------------------------------------------------------------------
/lib/realtime/realtimeUpdateDirtyNoteJob.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const config = require('../config')
4 | const logger = require('../logger')
5 | const moment = require('moment')
6 |
7 | class UpdateDirtyNoteJob {
8 | constructor (realtime) {
9 | this.realtime = realtime
10 | }
11 |
12 | start () {
13 | if (this.timer) return
14 | this.timer = setInterval(this.updateDirtyNotes.bind(this), 1000)
15 | }
16 |
17 | stop () {
18 | if (!this.timer) return
19 | clearInterval(this.timer)
20 | this.timer = undefined
21 | }
22 |
23 | updateDirtyNotes () {
24 | const notes = this.realtime.getNotePool()
25 | Object.keys(notes).forEach((key) => {
26 | const note = notes[key]
27 | this.updateDirtyNote(note)
28 | .catch((err) => {
29 | logger.error('updateDirtyNote: updater error', err)
30 | })
31 | })
32 | }
33 |
34 | async updateDirtyNote (note) {
35 | const notes = this.realtime.getNotePool()
36 | if (!note.server.isDirty) return
37 |
38 | if (config.debug) logger.info('updateDirtyNote: updater found dirty note: ' + note.id)
39 | note.server.isDirty = false
40 |
41 | try {
42 | const _note = await this.updateNoteAsync(note)
43 | // handle when note already been clean up
44 | if (!notes[note.id] || !notes[note.id].server) return
45 |
46 | if (!_note) {
47 | this.realtime.io.to(note.id).emit('info', {
48 | code: 404
49 | })
50 | logger.error('updateDirtyNote: note not found: ', note.id)
51 | this.realtime.disconnectSocketOnNote(note)
52 | }
53 |
54 | note.updatetime = moment(_note.lastchangeAt).valueOf()
55 | this.realtime.emitCheck(note)
56 | } catch (err) {
57 | logger.error('updateDirtyNote: note not found: ', note.id)
58 | this.realtime.io.to(note.id).emit('info', {
59 | code: 404
60 | })
61 | this.realtime.disconnectSocketOnNote(note)
62 | throw err
63 | }
64 | }
65 |
66 | updateNoteAsync (note) {
67 | return new Promise((resolve, reject) => {
68 | this.realtime.updateNote(note, (err, _note) => {
69 | if (err) {
70 | return reject(err)
71 | }
72 | return resolve(_note)
73 | })
74 | })
75 | }
76 | }
77 |
78 | exports.UpdateDirtyNoteJob = UpdateDirtyNoteJob
79 |
--------------------------------------------------------------------------------
/lib/auth/utils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const models = require('../models')
4 | const config = require('../config')
5 | const logger = require('../logger')
6 |
7 | exports.setReturnToFromReferer = function setReturnToFromReferer (req) {
8 | if (!req.session) req.session = {}
9 |
10 | var referer = req.get('referer')
11 | var nextURL
12 | if (referer) {
13 | try {
14 | var refererSearchParams = new URLSearchParams(new URL(referer).search)
15 | nextURL = refererSearchParams.get('next')
16 | } catch (err) {
17 | logger.warn(err)
18 | }
19 | }
20 |
21 | if (nextURL) {
22 | var isRelativeNextURL = nextURL.indexOf('://') === -1 && !nextURL.startsWith('//')
23 | if (isRelativeNextURL) {
24 | req.session.returnTo = (new URL(nextURL, config.serverURL)).toString()
25 | } else {
26 | req.session.returnTo = config.serverURL
27 | }
28 | } else {
29 | req.session.returnTo = referer
30 | }
31 | }
32 |
33 | exports.passportGeneralCallback = function callback (accessToken, refreshToken, profile, done) {
34 | var stringifiedProfile = JSON.stringify(profile)
35 | models.User.findOrCreate({
36 | where: {
37 | profileid: profile.id.toString()
38 | },
39 | defaults: {
40 | profile: stringifiedProfile,
41 | accessToken: accessToken,
42 | refreshToken: refreshToken
43 | }
44 | }).spread(function (user, created) {
45 | if (user) {
46 | var needSave = false
47 | if (user.profile !== stringifiedProfile) {
48 | user.profile = stringifiedProfile
49 | needSave = true
50 | }
51 | if (user.accessToken !== accessToken) {
52 | user.accessToken = accessToken
53 | needSave = true
54 | }
55 | if (user.refreshToken !== refreshToken) {
56 | user.refreshToken = refreshToken
57 | needSave = true
58 | }
59 | if (needSave) {
60 | user.save().then(function () {
61 | if (config.debug) { logger.info('user login: ' + user.id) }
62 | return done(null, user)
63 | })
64 | } else {
65 | if (config.debug) { logger.info('user login: ' + user.id) }
66 | return done(null, user)
67 | }
68 | }
69 | }).catch(function (err) {
70 | logger.error('auth callback failed: ' + err)
71 | return done(err, null)
72 | })
73 | }
74 |
--------------------------------------------------------------------------------
/public/views/codimd/head.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= title %>
9 |
10 |
11 | <% if(useCDN) { %>
12 |
13 |
14 |
15 |
16 |
17 |
18 | <%- include('../build/index-header') %>
19 | <%- include('../shared/polyfill') %>
20 | <% } else { %>
21 |
22 |
23 |
24 | <%- include('../build/index-pack-header') %>
25 | <% } %>
26 |
27 |
--------------------------------------------------------------------------------
/public/js/lib/common/login.js:
--------------------------------------------------------------------------------
1 | /* eslint-env browser, jquery */
2 | /* global Cookies */
3 |
4 | import { serverurl } from '../config'
5 |
6 | let checkAuth = false
7 | let profile = null
8 | let lastLoginState = getLoginState()
9 | let lastUserId = getUserId()
10 | var loginStateChangeEvent = null
11 |
12 | export function setloginStateChangeEvent (func) {
13 | loginStateChangeEvent = func
14 | }
15 |
16 | export function resetCheckAuth () {
17 | checkAuth = false
18 | }
19 |
20 | export function setLoginState (bool, id) {
21 | Cookies.set('loginstate', bool, {
22 | expires: 365
23 | })
24 | if (id) {
25 | Cookies.set('userid', id, {
26 | expires: 365
27 | })
28 | } else {
29 | Cookies.remove('userid')
30 | }
31 | lastLoginState = bool
32 | lastUserId = id
33 | checkLoginStateChanged()
34 | }
35 |
36 | export function checkLoginStateChanged () {
37 | if (getLoginState() !== lastLoginState || getUserId() !== lastUserId) {
38 | if (loginStateChangeEvent) setTimeout(loginStateChangeEvent, 100)
39 | return true
40 | } else {
41 | return false
42 | }
43 | }
44 |
45 | export function getLoginState () {
46 | const state = Cookies.get('loginstate')
47 | return state === 'true' || state === true
48 | }
49 |
50 | export function getUserId () {
51 | return Cookies.get('userid')
52 | }
53 |
54 | export function clearLoginState () {
55 | Cookies.remove('loginstate')
56 | }
57 |
58 | export function checkIfAuth (yesCallback, noCallback) {
59 | const cookieLoginState = getLoginState()
60 | if (checkLoginStateChanged()) checkAuth = false
61 | if (!checkAuth || typeof cookieLoginState === 'undefined') {
62 | $.get(`${serverurl}/me`)
63 | .done(data => {
64 | if (data && data.status === 'ok') {
65 | profile = data
66 | yesCallback(profile)
67 | setLoginState(true, data.id)
68 | } else {
69 | noCallback()
70 | setLoginState(false)
71 | }
72 | })
73 | .fail(() => {
74 | noCallback()
75 | })
76 | .always(() => {
77 | checkAuth = true
78 | })
79 | } else if (cookieLoginState) {
80 | yesCallback(profile)
81 | } else {
82 | noCallback()
83 | }
84 | }
85 |
86 | export default {
87 | checkAuth,
88 | profile,
89 | lastLoginState,
90 | lastUserId,
91 | loginStateChangeEvent
92 | }
93 |
--------------------------------------------------------------------------------
/lib/ot/wrapped-operation.js:
--------------------------------------------------------------------------------
1 | if (typeof ot === 'undefined') {
2 | // Export for browsers
3 | var ot = {};
4 | }
5 |
6 | ot.WrappedOperation = (function (global) {
7 | 'use strict';
8 |
9 | // A WrappedOperation contains an operation and corresponing metadata.
10 | function WrappedOperation (operation, meta) {
11 | this.wrapped = operation;
12 | this.meta = meta;
13 | }
14 |
15 | WrappedOperation.prototype.apply = function () {
16 | return this.wrapped.apply.apply(this.wrapped, arguments);
17 | };
18 |
19 | WrappedOperation.prototype.invert = function () {
20 | var meta = this.meta;
21 | return new WrappedOperation(
22 | this.wrapped.invert.apply(this.wrapped, arguments),
23 | meta && typeof meta === 'object' && typeof meta.invert === 'function' ?
24 | meta.invert.apply(meta, arguments) : meta
25 | );
26 | };
27 |
28 | // Copy all properties from source to target.
29 | function copy (source, target) {
30 | for (var key in source) {
31 | if (source.hasOwnProperty(key)) {
32 | target[key] = source[key];
33 | }
34 | }
35 | }
36 |
37 | function composeMeta (a, b) {
38 | if (a && typeof a === 'object') {
39 | if (typeof a.compose === 'function') { return a.compose(b); }
40 | var meta = {};
41 | copy(a, meta);
42 | copy(b, meta);
43 | return meta;
44 | }
45 | return b;
46 | }
47 |
48 | WrappedOperation.prototype.compose = function (other) {
49 | return new WrappedOperation(
50 | this.wrapped.compose(other.wrapped),
51 | composeMeta(this.meta, other.meta)
52 | );
53 | };
54 |
55 | function transformMeta (meta, operation) {
56 | if (meta && typeof meta === 'object') {
57 | if (typeof meta.transform === 'function') {
58 | return meta.transform(operation);
59 | }
60 | }
61 | return meta;
62 | }
63 |
64 | WrappedOperation.transform = function (a, b) {
65 | var transform = a.wrapped.constructor.transform;
66 | var pair = transform(a.wrapped, b.wrapped);
67 | return [
68 | new WrappedOperation(pair[0], transformMeta(a.meta, b.wrapped)),
69 | new WrappedOperation(pair[1], transformMeta(b.meta, a.wrapped))
70 | ];
71 | };
72 |
73 | return WrappedOperation;
74 |
75 | }(this));
76 |
77 | // Export for CommonJS
78 | if (typeof module === 'object') {
79 | module.exports = ot.WrappedOperation;
80 | }
--------------------------------------------------------------------------------
/public/vendor/ot/wrapped-operation.js:
--------------------------------------------------------------------------------
1 | if (typeof ot === 'undefined') {
2 | // Export for browsers
3 | var ot = {};
4 | }
5 |
6 | ot.WrappedOperation = (function (global) {
7 | 'use strict';
8 |
9 | // A WrappedOperation contains an operation and corresponing metadata.
10 | function WrappedOperation (operation, meta) {
11 | this.wrapped = operation;
12 | this.meta = meta;
13 | }
14 |
15 | WrappedOperation.prototype.apply = function () {
16 | return this.wrapped.apply.apply(this.wrapped, arguments);
17 | };
18 |
19 | WrappedOperation.prototype.invert = function () {
20 | var meta = this.meta;
21 | return new WrappedOperation(
22 | this.wrapped.invert.apply(this.wrapped, arguments),
23 | meta && typeof meta === 'object' && typeof meta.invert === 'function' ?
24 | meta.invert.apply(meta, arguments) : meta
25 | );
26 | };
27 |
28 | // Copy all properties from source to target.
29 | function copy (source, target) {
30 | for (var key in source) {
31 | if (source.hasOwnProperty(key)) {
32 | target[key] = source[key];
33 | }
34 | }
35 | }
36 |
37 | function composeMeta (a, b) {
38 | if (a && typeof a === 'object') {
39 | if (typeof a.compose === 'function') { return a.compose(b); }
40 | var meta = {};
41 | copy(a, meta);
42 | copy(b, meta);
43 | return meta;
44 | }
45 | return b;
46 | }
47 |
48 | WrappedOperation.prototype.compose = function (other) {
49 | return new WrappedOperation(
50 | this.wrapped.compose(other.wrapped),
51 | composeMeta(this.meta, other.meta)
52 | );
53 | };
54 |
55 | function transformMeta (meta, operation) {
56 | if (meta && typeof meta === 'object') {
57 | if (typeof meta.transform === 'function') {
58 | return meta.transform(operation);
59 | }
60 | }
61 | return meta;
62 | }
63 |
64 | WrappedOperation.transform = function (a, b) {
65 | var transform = a.wrapped.constructor.transform;
66 | var pair = transform(a.wrapped, b.wrapped);
67 | return [
68 | new WrappedOperation(pair[0], transformMeta(a.meta, b.wrapped)),
69 | new WrappedOperation(pair[1], transformMeta(b.meta, a.wrapped))
70 | ];
71 | };
72 |
73 | return WrappedOperation;
74 |
75 | }(this));
76 |
77 | // Export for CommonJS
78 | if (typeof module === 'object') {
79 | module.exports = ot.WrappedOperation;
80 | }
--------------------------------------------------------------------------------
/public/css/codemirror-extend/ayu-dark.css:
--------------------------------------------------------------------------------
1 | /* Based on https://github.com/dempfi/ayu */
2 |
3 | .cm-s-ayu-dark.CodeMirror { background: #0a0e14; color: #b3b1ad; }
4 | .cm-s-ayu-dark div.CodeMirror-selected { background: #273747; }
5 | .cm-s-ayu-dark .CodeMirror-line::selection, .cm-s-ayu-dark .CodeMirror-line > span::selection, .cm-s-ayu-dark .CodeMirror-line > span > span::selection { background: rgba(39, 55, 71, 99); }
6 | .cm-s-ayu-dark .CodeMirror-line::-moz-selection, .cm-s-ayu-dark .CodeMirror-line > span::-moz-selection, .cm-s-ayu-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(39, 55, 71, 99); }
7 | .cm-s-ayu-dark .CodeMirror-gutters { background: #0a0e14; border-right: 0px; }
8 | .cm-s-ayu-dark .CodeMirror-guttermarker { color: white; }
9 | .cm-s-ayu-dark .CodeMirror-guttermarker-subtle { color: #3d424d; }
10 | .cm-s-ayu-dark .CodeMirror-linenumber { color: #3d424d; }
11 | .cm-s-ayu-dark .CodeMirror-cursor { border-left: 1px solid #e6b450; }
12 |
13 | .cm-s-ayu-dark span.cm-comment { color: #626a73; }
14 | .cm-s-ayu-dark span.cm-atom { color: #ae81ff; }
15 | .cm-s-ayu-dark span.cm-number { color: #e6b450; }
16 |
17 | .cm-s-ayu-dark span.cm-comment.cm-attribute { color: #ffb454; }
18 | .cm-s-ayu-dark span.cm-comment.cm-def { color: rgba(57, 186, 230, 80); }
19 | .cm-s-ayu-dark span.cm-comment.cm-tag { color: #39bae6; }
20 | .cm-s-ayu-dark span.cm-comment.cm-type { color: #5998a6; }
21 |
22 | .cm-s-ayu-dark span.cm-property, .cm-s-ayu-dark span.cm-attribute { color: #ffb454; }
23 | .cm-s-ayu-dark span.cm-keyword { color: #ff8f40; }
24 | .cm-s-ayu-dark span.cm-builtin { color: #e6b450; }
25 | .cm-s-ayu-dark span.cm-string { color: #c2d94c; }
26 |
27 | .cm-s-ayu-dark span.cm-variable { color: #b3b1ad; }
28 | .cm-s-ayu-dark span.cm-variable-2 { color: #f07178; }
29 | .cm-s-ayu-dark span.cm-variable-3 { color: #39bae6; }
30 | .cm-s-ayu-dark span.cm-type { color: #ff8f40; }
31 | .cm-s-ayu-dark span.cm-def { color: #ffee99; }
32 | .cm-s-ayu-dark span.cm-bracket { color: #f8f8f2; }
33 | .cm-s-ayu-dark span.cm-tag { color: rgba(57, 186, 230, 80); }
34 | .cm-s-ayu-dark span.cm-header { color: #c2d94c; }
35 | .cm-s-ayu-dark span.cm-link { color: #39bae6; }
36 | .cm-s-ayu-dark span.cm-error { color: #ff3333; }
37 |
38 | .cm-s-ayu-dark .CodeMirror-activeline-background { background: #01060e; }
39 | .cm-s-ayu-dark .CodeMirror-matchingbracket {
40 | text-decoration: underline;
41 | color: white !important;
42 | }
43 |
--------------------------------------------------------------------------------
/test/realtime/saveRevisionJob.test.js:
--------------------------------------------------------------------------------
1 | /* eslint-env node, mocha */
2 | 'use strict'
3 |
4 | const assert = require('assert')
5 | const mock = require('mock-require')
6 | const sinon = require('sinon')
7 | const { removeModuleFromRequireCache, removeLibModuleCache } = require('./utils')
8 |
9 | describe('save revision job', function () {
10 | let clock
11 | let mockModels
12 | let realtime
13 | beforeEach(() => {
14 | removeLibModuleCache()
15 | mockModels = {
16 | Revision: {
17 | saveAllNotesRevision: sinon.stub()
18 | }
19 | }
20 | clock = sinon.useFakeTimers()
21 | mock('../../lib/processQueue', require('../testDoubles/ProcessQueueFake'))
22 | mock('../../lib/logger', {
23 | error: () => {},
24 | info: () => {}
25 | })
26 | mock('../../lib/history', {})
27 | mock('../../lib/models', mockModels)
28 | mock('../../lib/config', {
29 | debug: true
30 | })
31 | mock('../../lib/realtimeUpdateDirtyNoteJob', require('../testDoubles/realtimeJobStub'))
32 | mock('../../lib/realtimeCleanDanglingUserJob', require('../testDoubles/realtimeJobStub'))
33 | })
34 |
35 | afterEach(() => {
36 | clock.restore()
37 | removeModuleFromRequireCache('../../lib/realtime/realtime')
38 | removeModuleFromRequireCache('../../lib/realtime/realtimeSaveRevisionJob')
39 | mock.stopAll()
40 | sinon.restore()
41 | })
42 |
43 | it('should execute save revision job every 5 min', (done) => {
44 | mockModels.Revision.saveAllNotesRevision.callsFake((callback) => {
45 | callback(null, [])
46 | })
47 | realtime = require('../../lib/realtime/realtime')
48 | clock.tick(5 * 60 * 1000)
49 | clock.restore()
50 | setTimeout(() => {
51 | assert(mockModels.Revision.saveAllNotesRevision.called)
52 | assert(realtime.saveRevisionJob.getSaverSleep() === true)
53 | done()
54 | }, 50)
55 | })
56 |
57 | it('should not set saverSleep when more than 1 note save revision', (done) => {
58 | mockModels.Revision.saveAllNotesRevision.callsFake((callback) => {
59 | callback(null, [1])
60 | })
61 | realtime = require('../../lib/realtime/realtime')
62 | clock.tick(5 * 60 * 1000)
63 | clock.restore()
64 | setTimeout(() => {
65 | assert(mockModels.Revision.saveAllNotesRevision.called)
66 | assert(realtime.saveRevisionJob.getSaverSleep() === false)
67 | done()
68 | }, 50)
69 | })
70 | })
71 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/number4.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/js/lib/renderer/fretboard/svg/number4_h.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/public/css/codemirror-extend/ayu-mirage.css:
--------------------------------------------------------------------------------
1 | /* Based on https://github.com/dempfi/ayu */
2 |
3 | .cm-s-ayu-mirage.CodeMirror { background: #1f2430; color: #cbccc6; }
4 | .cm-s-ayu-mirage div.CodeMirror-selected { background: #34455a; }
5 | .cm-s-ayu-mirage .CodeMirror-line::selection, .cm-s-ayu-mirage .CodeMirror-line > span::selection, .cm-s-ayu-mirage .CodeMirror-line > span > span::selection { background: #34455a; }
6 | .cm-s-ayu-mirage .CodeMirror-line::-moz-selection, .cm-s-ayu-mirage .CodeMirror-line > span::-moz-selection, .cm-s-ayu-mirage .CodeMirror-line > span > span::-moz-selection { background: rgba(25, 30, 42, 99); }
7 | .cm-s-ayu-mirage .CodeMirror-gutters { background: #1f2430; border-right: 0px; }
8 | .cm-s-ayu-mirage .CodeMirror-guttermarker { color: white; }
9 | .cm-s-ayu-mirage .CodeMirror-guttermarker-subtle { color: rgba(112, 122, 140, 66); }
10 | .cm-s-ayu-mirage .CodeMirror-linenumber { color: rgba(61, 66, 77, 99); }
11 | .cm-s-ayu-mirage .CodeMirror-cursor { border-left: 1px solid #ffcc66; }
12 |
13 | .cm-s-ayu-mirage span.cm-comment { color: #5c6773; font-style:italic; }
14 | .cm-s-ayu-mirage span.cm-atom { color: #ae81ff; }
15 | .cm-s-ayu-mirage span.cm-number { color: #ffcc66; }
16 |
17 | .cm-s-ayu-mirage span.cm-comment.cm-attribute { color: #ffd580; }
18 | .cm-s-ayu-mirage span.cm-comment.cm-def { color: #d4bfff; }
19 | .cm-s-ayu-mirage span.cm-comment.cm-tag { color: #5ccfe6; }
20 | .cm-s-ayu-mirage span.cm-comment.cm-type { color: #5998a6; }
21 |
22 | .cm-s-ayu-mirage span.cm-property { color: #f29e74; }
23 | .cm-s-ayu-mirage span.cm-attribute { color: #ffd580; }
24 | .cm-s-ayu-mirage span.cm-keyword { color: #ffa759; }
25 | .cm-s-ayu-mirage span.cm-builtin { color: #ffcc66; }
26 | .cm-s-ayu-mirage span.cm-string { color: #bae67e; }
27 |
28 | .cm-s-ayu-mirage span.cm-variable { color: #cbccc6; }
29 | .cm-s-ayu-mirage span.cm-variable-2 { color: #f28779; }
30 | .cm-s-ayu-mirage span.cm-variable-3 { color: #5ccfe6; }
31 | .cm-s-ayu-mirage span.cm-type { color: #ffa759; }
32 | .cm-s-ayu-mirage span.cm-def { color: #ffd580; }
33 | .cm-s-ayu-mirage span.cm-bracket { color: rgba(92, 207, 230, 80); }
34 | .cm-s-ayu-mirage span.cm-tag { color: #5ccfe6; }
35 | .cm-s-ayu-mirage span.cm-header { color: #bae67e; }
36 | .cm-s-ayu-mirage span.cm-link { color: #5ccfe6; }
37 | .cm-s-ayu-mirage span.cm-error { color: #ff3333; }
38 |
39 | .cm-s-ayu-mirage .CodeMirror-activeline-background { background: #191e2a; }
40 | .cm-s-ayu-mirage .CodeMirror-matchingbracket {
41 | text-decoration: underline;
42 | color: white !important;
43 | }
44 |
--------------------------------------------------------------------------------