mirror of https://github.com/status-im/codimd.git
Merge branch 'master' into feature/slides-spotlight
This commit is contained in:
commit
d5a5ebc4d0
|
@ -8,12 +8,6 @@ env:
|
|||
|
||||
jobs:
|
||||
include:
|
||||
- env: task=npm-test
|
||||
node_js:
|
||||
- 6
|
||||
before_install:
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version "$YARN_VERSION"
|
||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
||||
- env: task=npm-test
|
||||
node_js:
|
||||
- 8
|
||||
|
|
383
README.md
383
README.md
|
@ -5,349 +5,74 @@ CodiMD
|
|||
[![version][github-version-badge]][github-release-page]
|
||||
[![POEditor][poeditor-image]][poeditor-url]
|
||||
|
||||
CodiMD lets you create real-time collaborative markdown notes on all platforms.
|
||||
Inspired by Hackpad, with more focus on speed and flexibility, and build from [HackMD](https://hackmd.io) source code.
|
||||
Feel free to contribute.
|
||||
CodiMD lets you collaborate in real-time with markdown.
|
||||
Built on [HackMD](https://hackmd.io) source code, CodiMD lets you host and control your team's content with speed and ease.
|
||||
|
||||
Thanks for using! :smile:
|
||||
## CodiMD - The Open Source HackMD
|
||||
[HackMD](https://hackmd.io) helps developers write better documents and build active communities with open collaboration.
|
||||
HackMD is built with one promise - **You own and control all your content**:
|
||||
- You should be able to easily [download all your online content at once](https://hackmd.io/c/news/%2Fs%2Fr1cx3a3SE).
|
||||
- Your content formatting should be portable as well. (That's why we choose [markdown](https://hackmd.io/features#Typography).)
|
||||
- You should be able to control your content's presentation with HTML, [slide mode](https://hackmd.io/p/slide-example), or [book mode](https://hackmd.io/c/book-example/).
|
||||
|
||||
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
||||
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
||||
# Table of Contents
|
||||
With the same promise of you owning your content, CodiMD is the free software version of [HackMD](https://hackmd.io), developed and opened source by the HackMD team with reduced features, so you can use CodiMD for your community and own your data. *(See the [origin of the name CodiMD](https://github.com/hackmdio/hackmd/issues/720).)*
|
||||
|
||||
- [HackMD CE became CodiMD](#hackmd-ce-became-codimd)
|
||||
- [Browsers Requirement](#browsers-requirement)
|
||||
- [Installation](#installation)
|
||||
- [Getting started (Native install)](#getting-started-native-install)
|
||||
- [Prerequisite](#prerequisite)
|
||||
- [Instructions](#instructions)
|
||||
- [Heroku Deployment](#heroku-deployment)
|
||||
- [Kubernetes](#kubernetes)
|
||||
- [Upgrade](#upgrade)
|
||||
- [Native setup](#native-setup)
|
||||
- [Configuration](#configuration)
|
||||
- [Environment variables (will overwrite other server configs)](#environment-variables-will-overwrite-other-server-configs)
|
||||
- [Application settings `config.json`](#application-settings-configjson)
|
||||
- [Third-party integration API key settings](#third-party-integration-api-key-settings)
|
||||
- [Third-party integration OAuth callback URLs](#third-party-integration-oauth-callback-urls)
|
||||
- [Developer Notes](#developer-notes)
|
||||
- [Structure](#structure)
|
||||
- [Operational Transformation](#operational-transformation)
|
||||
- [License](#license)
|
||||
CodiMD is perfect for open communities, while HackMD emphasizes on permission and access controls for commercial use cases.
|
||||
|
||||
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
||||
HackMD team is committed to keep CodiMD open source. All contributions are welcome!
|
||||
|
||||
# HackMD CE became CodiMD
|
||||
## Documentation
|
||||
You would find all documentation here: [CodiMD Documentation](https://hackmd.io/c/codimd-documentation)
|
||||
|
||||
CodiMD was recently renamed from its former name was HackMD. CodiMD is the free software version of HackMD. HackMD EE, which is a SaaS (Software as a Service) product available at [hackmd.io](https://hackmd.io).
|
||||
### Deployment
|
||||
If you want to spin up an instance and start using immediately, see [Docker deployment](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-documentation#Deployment).
|
||||
If you want to contribute to the project, start with [manual deployment](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-manual-deployment).
|
||||
|
||||
We decided to change the name to break the confusion between HackMD and CodiMD, formally known as HackMD CE, as it never was an open core project.
|
||||
### Configuration
|
||||
CodiMD is highly customizable, learn about all configuration options of networking, security, performance, resources, privilege, privacy, image storage, and authentication in [CodiMD Configuration](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-configuration).
|
||||
|
||||
*For the whole renaming story, see the [related issue](https://github.com/hackmdio/hackmd/issues/720)*
|
||||
### Upgrading and Migration
|
||||
Upgrade CodiMD from previous version? See [this guide](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-upgrade)
|
||||
Migrating from Etherpad? Follow [this guide](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-migration-etherpad)
|
||||
|
||||
# Browsers Requirement
|
||||
### Developer
|
||||
Join our contributor community! Start from deploying [CodiMD manually](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-manual-deployment), [connecting to your own database](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-db-connection), [learn about the project structure](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-project-structure), to [build your changes](https://hackmd.io/c/codimd-documentation/%2Fs%2Fcodimd-webpack) with the help of webpack.
|
||||
|
||||
- ![Chrome](http://browserbadge.com/chrome/47/18px) Chrome >= 47, Chrome for Android >= 47
|
||||
- ![Safari](http://browserbadge.com/safari/9/18px) Safari >= 9, iOS Safari >= 8.4
|
||||
- ![Firefox](http://browserbadge.com/firefox/44/18px) Firefox >= 44
|
||||
- ![IE](http://browserbadge.com/ie/9/18px) IE >= 9, Edge >= 12
|
||||
- ![Opera](http://browserbadge.com/opera/34/18px) Opera >= 34, Opera Mini not supported
|
||||
## Contribution and Discussion
|
||||
All contributions are welcome! Even asking a question helps.
|
||||
|
||||
| Project | Contribution Types | Contribution Venue |
|
||||
| ------- | ------------------ | ------------------ |
|
||||
|**CodiMD**|:couple: Community chat|[Gitter](https://gitter.im/hackmdio/hackmd)|
|
||||
||:bug: Issues, bugs, and feature requests|[Issue tracker](https://github.com/hackmdio/codimd/issues)|
|
||||
||:books: Improve documentation|[Documentations](https://hackmd.io/c/codimd-documentation)|
|
||||
||:pencil: Translation|[POEditor](https://poeditor.com/join/project/q0nuPWyztp)|
|
||||
||:coffee: Donation|[Buy us coffee](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=KDGS4PREHX6QQ&lc=US&item_name=HackMD¤cy_code=USD&bn=PP%2dDonationsBF%3abtn_donate_LG%2egif%3aNonHosted)|
|
||||
|**HackMD**|:question: Issues related to [HackMD](https://hackmd.io/)|[Issue tracker](https://github.com/hackmdio/hackmd-io-issues/issues)|
|
||||
||:pencil2: Translation|[hackmd-locales](https://github.com/hackmdio/hackmd-locales/tree/master/locales)|
|
||||
|
||||
## Browser Support
|
||||
|
||||
CodiMD is a service that runs on Node.js, while users use the service through browsers. We support your users using the following browsers:
|
||||
- ![Chrome](http://browserbadge.com/chrome/47/18px)
|
||||
- Chrome >= 47
|
||||
- Chrome for Android >= 47
|
||||
- ![Safari](http://browserbadge.com/safari/9/18px)
|
||||
- Safari >= 9
|
||||
- iOS Safari >= 8.4
|
||||
- ![Firefox](http://browserbadge.com/firefox/44/18px)
|
||||
- Firefox >= 44
|
||||
- ![IE](http://browserbadge.com/ie/9/18px)
|
||||
- IE >= 9
|
||||
- Edge >= 12
|
||||
- ![Opera](http://browserbadge.com/opera/34/18px)
|
||||
- Opera >= 34
|
||||
- Opera Mini not supported
|
||||
- Android Browser >= 4.4
|
||||
|
||||
# Installation
|
||||
|
||||
## Getting started (Native install)
|
||||
|
||||
### Prerequisite
|
||||
|
||||
- Node.js 6.x or up (test up to 7.5.0) and <10.x
|
||||
- Database (PostgreSQL, MySQL, MariaDB, SQLite, MSSQL) use charset `utf8`
|
||||
- npm (and its dependencies, especially [uWebSockets](https://github.com/uWebSockets/uWebSockets#nodejs-developers), [node-gyp](https://github.com/nodejs/node-gyp#installation))
|
||||
- `libssl-dev` for building scrypt (see [here](https://github.com/ml1nk/node-scrypt/blob/master/README.md#installation-instructions) for further information)
|
||||
- For **building** CodiMD we recommend to use a machine with at least **2GB** RAM
|
||||
|
||||
### Instructions
|
||||
|
||||
1. Download a release and unzip or clone into a directory
|
||||
2. Enter the directory and type `bin/setup`, which will install npm dependencies and create configs. The setup script is written in Bash, you would need bash as a prerequisite.
|
||||
3. Setup the configs, see more below
|
||||
4. Setup environment variables which will overwrite the configs
|
||||
5. Build front-end bundle by `npm run build` (use `npm run dev` if you are in development)
|
||||
6. Modify the file named `.sequelizerc`, change the value of the variable `url` with your db connection string
|
||||
For example: `postgres://username:password@localhost:5432/codimd`
|
||||
7. Run `node_modules/.bin/sequelize db:migrate`, this step will migrate your db to the latest schema
|
||||
8. Run the server as you like (node, forever, pm2)
|
||||
|
||||
To stay up to date with your installation it's recommended to subscribe the [release feed][github-release-feed].
|
||||
|
||||
## Heroku Deployment
|
||||
|
||||
You can quickly setup a sample Heroku CodiMD application by clicking the button below.
|
||||
|
||||
[![Deploy on Heroku](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/hackmdio/codimd/tree/master)
|
||||
|
||||
If you deploy it without the button, keep in mind to use the right buildpacks. For details check `app.json`.
|
||||
|
||||
## Kubernetes
|
||||
|
||||
To install use `helm install stable/hackmd`.
|
||||
|
||||
For all further details, please check out the offical CodiMD [K8s helm chart](https://github.com/kubernetes/charts/tree/master/stable/hackmd).
|
||||
|
||||
**Debian-based version:**
|
||||
|
||||
[![latest](https://images.microbadger.com/badges/version/hackmdio/hackmd:latest.svg)](https://microbadger.com/images/hackmdio/hackmd "Get your own version badge on microbadger.com") [![](https://images.microbadger.com/badges/image/hackmdio/hackmd:latest.svg)](https://microbadger.com/images/hackmdio/hackmd "Get your own image badge on microbadger.com")
|
||||
|
||||
The easiest way to setup CodiMD using docker are using the following three commands:
|
||||
|
||||
```console
|
||||
git clone https://github.com/hackmdio/docker-hackmd.git
|
||||
cd docker-hackmd
|
||||
docker-compose up
|
||||
```
|
||||
Read more about it in the [container repository…](https://github.com/hackmdio/docker-hackmd)
|
||||
|
||||
# Upgrade
|
||||
|
||||
## Native setup
|
||||
|
||||
If you are upgrading CodiMD from an older version, follow these steps:
|
||||
|
||||
1. Fully stop your old server first (important)
|
||||
2. `git pull` or do whatever that updates the files
|
||||
3. `npm install` to update dependencies
|
||||
4. Build front-end bundle by `npm run build` (use `npm run dev` if you are in development)
|
||||
5. Modify the file named `.sequelizerc`, change the value of the variable `url` with your db connection string
|
||||
For example: `postgres://username:password@localhost:5432/codimd`
|
||||
6. Run `node_modules/.bin/sequelize db:migrate`, this step will migrate your db to the latest schema
|
||||
7. Start your whole new server!
|
||||
|
||||
To stay up to date with your installation it's recommended to subscribe the [release feed][github-release-feed].
|
||||
|
||||
* **migrate-to-1.1.0**
|
||||
|
||||
We deprecated the older lower case config style and moved on to camel case style. Please have a look at the current `config.json.example` and check the warnings on startup.
|
||||
|
||||
*Notice: This is not a breaking change right now but in the future*
|
||||
|
||||
* [**migration-to-0.5.0**](https://github.com/hackmdio/migration-to-0.5.0)
|
||||
|
||||
We don't use LZString to compress socket.io data and DB data after version 0.5.0.
|
||||
Please run the migration tool if you're upgrading from the old version.
|
||||
|
||||
* [**migration-to-0.4.0**](https://github.com/hackmdio/migration-to-0.4.0)
|
||||
|
||||
We've dropped MongoDB after version 0.4.0.
|
||||
So here is the migration tool for you to transfer the old DB data to the new DB.
|
||||
This tool is also used for official service.
|
||||
|
||||
# Configuration
|
||||
|
||||
There are some config settings you need to change in the files below.
|
||||
|
||||
```
|
||||
./config.json ----application settings
|
||||
```
|
||||
|
||||
## Environment variables (will overwrite other server configs)
|
||||
|
||||
| variables | example values | description |
|
||||
| --------- | ------ | ----------- |
|
||||
| `NODE_ENV` | `production` or `development` | set current environment (will apply corresponding settings in the `config.json`) |
|
||||
| `DEBUG` | `true` or `false` | set debug mode; show more logs |
|
||||
| `CMD_CONFIG_FILE` | `/path/to/config.json` | optional override for the path to CodiMD's config file |
|
||||
| `CMD_DOMAIN` | `codimd.org` | domain name |
|
||||
| `CMD_URL_PATH` | `codimd` | sub URL path, like `www.example.com/<URL_PATH>` |
|
||||
| `CMD_HOST` | `localhost` | host to listen on |
|
||||
| `CMD_PORT` | `80` | web app port |
|
||||
| `CMD_PATH` | `/var/run/codimd.sock` | path to UNIX domain socket to listen on (if specified, `CMD_HOST` and `CMD_PORT` are ignored) |
|
||||
| `CMD_LOGLEVEL` | `info` | Defines what kind of logs are provided to stdout. |
|
||||
| `CMD_ALLOW_ORIGIN` | `localhost, codimd.org` | domain name whitelist (use comma to separate) |
|
||||
| `CMD_PROTOCOL_USESSL` | `true` or `false` | set to use SSL protocol for resources path (only applied when domain is set) |
|
||||
| `CMD_URL_ADDPORT` | `true` or `false` | set to add port on callback URL (ports `80` or `443` won't be applied) (only applied when domain is set) |
|
||||
| `CMD_USECDN` | `true` or `false` | set to use CDN resources or not (default is `true`) |
|
||||
| `CMD_ALLOW_ANONYMOUS` | `true` or `false` | set to allow anonymous usage (default is `true`) |
|
||||
| `CMD_ALLOW_ANONYMOUS_EDITS` | `true` or `false` | if `allowAnonymous` is `true`, allow users to select `freely` permission, allowing guests to edit existing notes (default is `false`) |
|
||||
| `CMD_ALLOW_FREEURL` | `true` or `false` | set to allow new note creation by accessing a nonexistent note URL |
|
||||
| `CMD_FORBIDDEN_NODE_IDS` | `'robots.txt'` | disallow creation of notes, even if `CMD_ALLOW_FREEURL` is `true` |
|
||||
| `CMD_DEFAULT_PERMISSION` | `freely`, `editable`, `limited`, `locked` or `private` | set notes default permission (only applied on signed users) |
|
||||
| `CMD_DB_URL` | `mysql://localhost:3306/database` | set the database URL |
|
||||
| `CMD_SESSION_SECRET` | no example | Secret used to sign the session cookie. If non is set, one will randomly generated on startup |
|
||||
| `CMD_SESSION_LIFE` | `1209600000` | Session life time. (milliseconds) |
|
||||
| `CMD_FACEBOOK_CLIENTID` | no example | Facebook API client id |
|
||||
| `CMD_FACEBOOK_CLIENTSECRET` | no example | Facebook API client secret |
|
||||
| `CMD_TWITTER_CONSUMERKEY` | no example | Twitter API consumer key |
|
||||
| `CMD_TWITTER_CONSUMERSECRET` | no example | Twitter API consumer secret |
|
||||
| `CMD_GITHUB_CLIENTID` | no example | GitHub API client id |
|
||||
| `CMD_GITHUB_CLIENTSECRET` | no example | GitHub API client secret |
|
||||
| `CMD_GITLAB_SCOPE` | `read_user` or `api` | GitLab API requested scope (default is `api`) (GitLab snippet import/export need `api` scope) |
|
||||
| `CMD_GITLAB_BASEURL` | no example | GitLab authentication endpoint, set to use other endpoint than GitLab.com (optional) |
|
||||
| `CMD_GITLAB_CLIENTID` | no example | GitLab API client id |
|
||||
| `CMD_GITLAB_CLIENTSECRET` | no example | GitLab API client secret |
|
||||
| `CMD_GITLAB_VERSION` | no example | GitLab API version (v3 or v4) |
|
||||
| `CMD_MATTERMOST_BASEURL` | no example | Mattermost authentication endpoint for versions below 5.0. For Mattermost version 5.0 and above, see [guide](docs/guides/auth/mattermost-self-hosted.md). |
|
||||
| `CMD_MATTERMOST_CLIENTID` | no example | Mattermost API client id |
|
||||
| `CMD_MATTERMOST_CLIENTSECRET` | no example | Mattermost API client secret |
|
||||
| `CMD_DROPBOX_CLIENTID` | no example | Dropbox API client id |
|
||||
| `CMD_DROPBOX_CLIENTSECRET` | no example | Dropbox API client secret |
|
||||
| `CMD_GOOGLE_CLIENTID` | no example | Google API client id |
|
||||
| `CMD_GOOGLE_CLIENTSECRET` | no example | Google API client secret |
|
||||
| `CMD_LDAP_URL` | `ldap://example.com` | URL of LDAP server |
|
||||
| `CMD_LDAP_BINDDN` | no example | bindDn for LDAP access |
|
||||
| `CMD_LDAP_BINDCREDENTIALS` | no example | bindCredentials for LDAP access |
|
||||
| `CMD_LDAP_SEARCHBASE` | `o=users,dc=example,dc=com` | LDAP directory to begin search from |
|
||||
| `CMD_LDAP_SEARCHFILTER` | `(uid={{username}})` | LDAP filter to search with |
|
||||
| `CMD_LDAP_SEARCHATTRIBUTES` | `displayName, mail` | LDAP attributes to search with (use comma to separate) |
|
||||
| `CMD_LDAP_USERIDFIELD` | `uidNumber` or `uid` or `sAMAccountName` | The LDAP field which is used uniquely identify a user on CodiMD |
|
||||
| `CMD_LDAP_USERNAMEFIELD` | Fallback to userid | The LDAP field which is used as the username on CodiMD |
|
||||
| `CMD_LDAP_TLS_CA` | `server-cert.pem, root.pem` | Root CA for LDAP TLS in PEM format (use comma to separate) |
|
||||
| `CMD_LDAP_PROVIDERNAME` | `My institution` | Optional name to be displayed at login form indicating the LDAP provider |
|
||||
| `CMD_SAML_IDPSSOURL` | `https://idp.example.com/sso` | authentication endpoint of IdP. for details, see [guide](docs/guides/auth/saml-onelogin.md). |
|
||||
| `CMD_SAML_IDPCERT` | `/path/to/cert.pem` | certificate file path of IdP in PEM format |
|
||||
| `CMD_SAML_ISSUER` | no example | identity of the service provider (optional, default: serverurl)" |
|
||||
| `CMD_SAML_IDENTIFIERFORMAT` | no example | name identifier format (optional, default: `urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress`) |
|
||||
| `CMD_SAML_GROUPATTRIBUTE` | `memberOf` | attribute name for group list (optional) |
|
||||
| `CMD_SAML_REQUIREDGROUPS` | `Hackmd-users` | group names that allowed (use vertical bar to separate) (optional) |
|
||||
| `CMD_SAML_EXTERNALGROUPS` | `Temporary-staff` | group names that not allowed (use vertical bar to separate) (optional) |
|
||||
| `CMD_SAML_ATTRIBUTE_ID` | `sAMAccountName` | attribute map for `id` (optional, default: NameID of SAML response) |
|
||||
| `CMD_SAML_ATTRIBUTE_USERNAME` | `mailNickname` | attribute map for `username` (optional, default: NameID of SAML response) |
|
||||
| `CMD_SAML_ATTRIBUTE_EMAIL` | `mail` | attribute map for `email` (optional, default: NameID of SAML response if `CMD_SAML_IDENTIFIERFORMAT` is default) |
|
||||
| `CMD_OAUTH2_USER_PROFILE_URL` | `https://example.com` | where retrieve information about a user after succesful login. Needs to output JSON. (no default value) Refer to the [Mattermost](docs/guides/auth/mattermost-self-hosted.md) or [Nextcloud](docs/guides/auth/nextcloud.md) examples for more details on all of the `CMD_OAUTH2...` options. |
|
||||
| `CMD_OAUTH2_USER_PROFILE_USERNAME_ATTR` | `name` | where to find the username in the JSON from the user profile URL. (no default value)|
|
||||
| `CMD_OAUTH2_USER_PROFILE_DISPLAY_NAME_ATTR` | `display-name` | where to find the display-name in the JSON from the user profile URL. (no default value) |
|
||||
| `CMD_OAUTH2_USER_PROFILE_EMAIL_ATTR` | `email` | where to find the email address in the JSON from the user profile URL. (no default value) |
|
||||
| `CMD_OAUTH2_TOKEN_URL` | `https://example.com` | sometimes called token endpoint, please refer to the documentation of your OAuth2 provider (no default value) |
|
||||
| `CMD_OAUTH2_AUTHORIZATION_URL` | `https://example.com` | authorization URL of your provider, please refer to the documentation of your OAuth2 provider (no default value) |
|
||||
| `CMD_OAUTH2_CLIENT_ID` | `afae02fckafd...` | you will get this from your OAuth2 provider when you register CodiMD as OAuth2-client, (no default value) |
|
||||
| `CMD_OAUTH2_CLIENT_SECRET` | `afae02fckafd...` | you will get this from your OAuth2 provider when you register CodiMD as OAuth2-client, (no default value) |
|
||||
| `CMD_OAUTH2_PROVIDERNAME` | `My institution` | Optional name to be displayed at login form indicating the oAuth2 provider |
|
||||
| `CMD_IMGUR_CLIENTID` | no example | Imgur API client id |
|
||||
| `CMD_EMAIL` | `true` or `false` | set to allow email signin |
|
||||
| `CMD_ALLOW_PDF_EXPORT` | `true` or `false` | Enable or disable PDF exports |
|
||||
| `CMD_ALLOW_EMAIL_REGISTER` | `true` or `false` | set to allow email register (only applied when email is set, default is `true`. Note `bin/manage_users` might help you if registration is `false`.) |
|
||||
| `CMD_ALLOW_GRAVATAR` | `true` or `false` | set to `false` to disable gravatar as profile picture source on your instance |
|
||||
| `CMD_IMAGE_UPLOAD_TYPE` | `imgur`, `s3`, `minio` or `filesystem` | Where to upload images. For S3, see our Image Upload Guides for [S3](docs/guides/s3-image-upload.md) or [Minio](docs/guides/minio-image-upload.md) |
|
||||
| `CMD_S3_ACCESS_KEY_ID` | no example | AWS access key id |
|
||||
| `CMD_S3_SECRET_ACCESS_KEY` | no example | AWS secret key |
|
||||
| `CMD_S3_REGION` | `ap-northeast-1` | AWS S3 region |
|
||||
| `CMD_S3_BUCKET` | no example | AWS S3 bucket name |
|
||||
| `CMD_MINIO_ACCESS_KEY` | no example | Minio access key |
|
||||
| `CMD_MINIO_SECRET_KEY` | no example | Minio secret key |
|
||||
| `CMD_MINIO_ENDPOINT` | `minio.example.org` | Address of your Minio endpoint/instance |
|
||||
| `CMD_MINIO_PORT` | `9000` | Port that is used for your Minio instance |
|
||||
| `CMD_MINIO_SECURE` | `true` | If set to `true` HTTPS is used for Minio |
|
||||
| `CMD_AZURE_CONNECTION_STRING` | no example | Azure Blob Storage connection string |
|
||||
| `CMD_AZURE_CONTAINER` | no example | Azure Blob Storage container name (automatically created if non existent) |
|
||||
| `CMD_HSTS_ENABLE` | ` true` | set to enable [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) if HTTPS is also enabled (default is ` true`) |
|
||||
| `CMD_HSTS_INCLUDE_SUBDOMAINS` | `true` | set to include subdomains in HSTS (default is `true`) |
|
||||
| `CMD_HSTS_MAX_AGE` | `31536000` | max duration in seconds to tell clients to keep HSTS status (default is a year) |
|
||||
| `CMD_HSTS_PRELOAD` | `true` | whether to allow preloading of the site's HSTS status (e.g. into browsers) |
|
||||
| `CMD_CSP_ENABLE` | `true` | whether to enable Content Security Policy (directives cannot be configured with environment variables) |
|
||||
| `CMD_CSP_REPORTURI` | `https://<someid>.report-uri.com/r/d/csp/enforce` | Allows to add a URL for CSP reports in case of violations |
|
||||
| `CMD_SOURCE_URL` | `https://github.com/hackmdio/codimd/tree/<current commit>` | Provides the link to the source code of CodiMD on the entry page (Please, make sure you change this when you run a modified version) |
|
||||
|
||||
***Note:** Due to the rename process we renamed all `HMD_`-prefix variables to be `CMD_`-prefixed. The old ones continue to work.*
|
||||
|
||||
## Application settings `config.json`
|
||||
|
||||
| variables | example values | description |
|
||||
| --------- | ------ | ----------- |
|
||||
| `debug` | `true` or `false` | set debug mode, show more logs |
|
||||
| `domain` | `localhost` | domain name |
|
||||
| `urlPath` | `codimd` | sub URL path, like `www.example.com/<urlpath>` |
|
||||
| `host` | `localhost` | host to listen on |
|
||||
| `port` | `80` | web app port |
|
||||
| `path` | `/var/run/codimd.sock` | path to UNIX domain socket to listen on (if specified, `host` and `port` are ignored) |
|
||||
| `loglevel` | `info` | Defines what kind of logs are provided to stdout. |
|
||||
| `allowOrigin` | `['localhost']` | domain name whitelist |
|
||||
| `useSSL` | `true` or `false` | set to use SSL server (if `true`, will auto turn on `protocolUseSSL`) |
|
||||
| `hsts` | `{"enable": true, "maxAgeSeconds": 31536000, "includeSubdomains": true, "preload": true}` | [HSTS](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) options to use with HTTPS (default is the example value, max age is a year) |
|
||||
| `csp` | `{"enable": true, "directives": {"scriptSrc": "trustworthy-scripts.example.com"}, "upgradeInsecureRequests": "auto", "addDefaults": true}` | Configures [Content Security Policy](https://helmetjs.github.io/docs/csp/). Directives are passed to Helmet - see [their documentation](https://helmetjs.github.io/docs/csp/) for more information on the format. Some defaults are added to the configured values so that the application doesn't break. To disable this behaviour, set `addDefaults` to `false`. Further, if `usecdn` is on, some CDN locations are allowed too. By default (`auto`), insecure (HTTP) requests are upgraded to HTTPS via CSP if `useSSL` is on. To change this behaviour, set `upgradeInsecureRequests` to either `true` or `false`. |
|
||||
| `protocolUseSSL` | `true` or `false` | set to use SSL protocol for resources path (only applied when domain is set) |
|
||||
| `urlAddPort` | `true` or `false` | set to add port on callback URL (ports `80` or `443` won't be applied) (only applied when domain is set) |
|
||||
| `useCDN` | `true` or `false` | set to use CDN resources or not (default is `true`) |
|
||||
| `allowAnonymous` | `true` or `false` | set to allow anonymous usage (default is `true`) |
|
||||
| `allowAnonymousEdits` | `true` or `false` | if `allowAnonymous` is `true`: allow users to select `freely` permission, allowing guests to edit existing notes (default is `false`) |
|
||||
| `allowFreeURL` | `true` or `false` | set to allow new note creation by accessing a nonexistent note URL |
|
||||
| `forbiddenNoteIDs` | `['robots.txt']` | disallow creation of notes, even if `allowFreeUrl` is `true` |
|
||||
| `defaultPermission` | `freely`, `editable`, `limited`, `locked`, `protected` or `private` | set notes default permission (only applied on signed users) |
|
||||
| `dbURL` | `mysql://localhost:3306/database` | set the db URL; if set, then db config (below) won't be applied |
|
||||
| `db` | `{ "dialect": "sqlite", "storage": "./db.codimd.sqlite" }` | set the db configs, [see more here](http://sequelize.readthedocs.org/en/latest/api/sequelize/) |
|
||||
| `sslKeyPath` | `./cert/client.key` | SSL key path<sup>1</sup> (only need when you set `useSSL`) |
|
||||
| `sslCertPath` | `./cert/codimd_io.crt` | SSL cert path<sup>1</sup> (only need when you set `useSSL`) |
|
||||
| `sslCAPath` | `['./cert/COMODORSAAddTrustCA.crt']` | SSL ca chain<sup>1</sup> (only need when you set `useSSL`) |
|
||||
| `dhParamPath` | `./cert/dhparam.pem` | SSL dhparam path<sup>1</sup> (only need when you set `useSSL`) |
|
||||
| `tmpPath` | `./tmp/` | temp directory path<sup>1</sup> |
|
||||
| `defaultNotePath` | `./public/default.md` | default note file path<sup>1</sup> |
|
||||
| `docsPath` | `./public/docs` | docs directory path<sup>1</sup> |
|
||||
| `viewPath` | `./public/views` | template directory path<sup>1</sup> |
|
||||
| `uploadsPath` | `./public/uploads` | uploads directory<sup>1</sup> - needs to be persistent when you use imageUploadType `filesystem` |
|
||||
| `sessionName` | `connect.sid` | cookie session name |
|
||||
| `sessionSecret` | `secret` | cookie session secret |
|
||||
| `sessionLife` | `14 * 24 * 60 * 60 * 1000` | cookie session life |
|
||||
| `staticCacheTime` | `1 * 24 * 60 * 60 * 1000` | static file cache time |
|
||||
| `heartbeatInterval` | `5000` | socket.io heartbeat interval |
|
||||
| `heartbeatTimeout` | `10000` | socket.io heartbeat timeout |
|
||||
| `documentMaxLength` | `100000` | note max length |
|
||||
| `email` | `true` or `false` | set to allow email signin |
|
||||
| `oauth2` | `{baseURL: ..., userProfileURL: ..., userProfileUsernameAttr: ..., userProfileDisplayNameAttr: ..., userProfileEmailAttr: ..., tokenURL: ..., authorizationURL: ..., clientID: ..., clientSecret: ...}` | An object detailing your OAuth2 provider. Refer to the [Mattermost](docs/guides/auth/mattermost-self-hosted.md) or [Nextcloud](docs/guides/auth/nextcloud.md) examples for more details!|
|
||||
| `allowEmailRegister` | `true` or `false` | set to allow email register (only applied when email is set, default is `true`. Note `bin/manage_users` might help you if registration is `false`.) |
|
||||
| `allowGravatar` | `true` or `false` | set to `false` to disable gravatar as profile picture source on your instance |
|
||||
| `imageUploadType` | `imgur`, `s3`, `minio`, `azure` or `filesystem`(default) | Where to upload images. For S3, see our Image Upload Guides for [S3](docs/guides/s3-image-upload.md) or [Minio](docs/guides/minio-image-upload.md)|
|
||||
| `minio` | `{ "accessKey": "YOUR_MINIO_ACCESS_KEY", "secretKey": "YOUR_MINIO_SECRET_KEY", "endpoint": "YOUR_MINIO_HOST", port: 9000, secure: true }` | When `imageUploadType` is set to `minio`, you need to set this key. Also checkout our [Minio Image Upload Guide](docs/guides/minio-image-upload.md) |
|
||||
| `s3` | `{ "accessKeyId": "YOUR_S3_ACCESS_KEY_ID", "secretAccessKey": "YOUR_S3_ACCESS_KEY", "region": "YOUR_S3_REGION" }` | When `imageuploadtype` be set to `s3`, you would also need to setup this key, check our [S3 Image Upload Guide](docs/guides/s3-image-upload.md) |
|
||||
| `s3bucket` | `YOUR_S3_BUCKET_NAME` | bucket name when `imageUploadType` is set to `s3` or `minio` |
|
||||
| `sourceURL` | `https://github.com/hackmdio/codimd/tree/<current commit>` | Provides the link to the source code of CodiMD on the entry page (Please, make sure you change this when you run a modified version) |
|
||||
|
||||
<sup>1</sup>: relative paths are based on CodiMD's base directory
|
||||
|
||||
## Third-party integration API key settings
|
||||
|
||||
| service | settings location | description |
|
||||
| ------- | --------- | ----------- |
|
||||
| facebook, twitter, github, gitlab, mattermost, dropbox, google, ldap, saml | environment variables or `config.json` | for signin |
|
||||
| imgur, s3, minio, azure | environment variables or `config.json` | for image upload |
|
||||
| dropbox(`dropbox/appKey`) | `config.json` | for export and import |
|
||||
|
||||
## Third-party integration OAuth callback URLs
|
||||
|
||||
| service | callback URL (after the server URL) |
|
||||
| ------- | --------- |
|
||||
| facebook | `/auth/facebook/callback` |
|
||||
| twitter | `/auth/twitter/callback` |
|
||||
| github | `/auth/github/callback` |
|
||||
| gitlab | `/auth/gitlab/callback` |
|
||||
| mattermost | `/auth/mattermost/callback` |
|
||||
| dropbox | `/auth/dropbox/callback` |
|
||||
| google | `/auth/google/callback` |
|
||||
| saml | `/auth/saml/callback` |
|
||||
|
||||
# Developer Notes
|
||||
|
||||
## Structure
|
||||
|
||||
```text
|
||||
codimd/
|
||||
├── tmp/ --- temporary files
|
||||
├── docs/ --- document files
|
||||
├── lib/ --- server libraries
|
||||
└── public/ --- client files
|
||||
├── css/ --- css styles
|
||||
├── js/ --- js scripts
|
||||
├── vendor/ --- vendor includes
|
||||
└── views/ --- view templates
|
||||
```
|
||||
|
||||
## Operational Transformation
|
||||
|
||||
From 0.3.2, we started supporting operational transformation.
|
||||
It makes concurrent editing safe and will not break up other users' operations.
|
||||
Additionally, now can show other clients' selections.
|
||||
See more at [http://operational-transformation.github.io/](http://operational-transformation.github.io/)
|
||||
|
||||
|
||||
|
||||
# License
|
||||
## License
|
||||
|
||||
**License under AGPL.**
|
||||
|
||||
|
|
150
bin/manage_users
150
bin/manage_users
|
@ -1,119 +1,117 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
// First configure the logger so it does not spam the console
|
||||
const logger = require("../lib/logger");
|
||||
logger.transports.forEach((transport) => transport.level = "warning")
|
||||
const logger = require('../lib/logger')
|
||||
logger.transports.forEach((transport) => {
|
||||
transport.level = 'warning'
|
||||
})
|
||||
|
||||
const models = require("../lib/models/");
|
||||
const readline = require("readline-sync");
|
||||
const minimist = require("minimist");
|
||||
const models = require('../lib/models/')
|
||||
const readline = require('readline-sync')
|
||||
const minimist = require('minimist')
|
||||
|
||||
function showUsage(tips) {
|
||||
console.log(`${tips}
|
||||
function showUsage (tips) {
|
||||
console.log(`${tips}
|
||||
|
||||
Command-line utility to create users for email-signin.
|
||||
|
||||
Usage: bin/manage_users [--pass password] (--add | --del) user-email
|
||||
Options:
|
||||
--add Add user with the specified user-email
|
||||
--del Delete user with specified user-email
|
||||
--reset Reset user password with specified user-email
|
||||
--pass Use password from cmdline rather than prompting
|
||||
`);
|
||||
process.exit(1);
|
||||
Options:
|
||||
--add\tAdd user with the specified user-email
|
||||
--del\tDelete user with specified user-email
|
||||
--reset\tReset user password with specified user-email
|
||||
--pass\tUse password from cmdline rather than prompting
|
||||
`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
function getPass(argv, action) {
|
||||
// Find whether we use cmdline or prompt password
|
||||
if(typeof argv["pass"] !== 'string') {
|
||||
return readline.question(`Password for ${argv[action]}:`, {hideEchoBack: true});
|
||||
}
|
||||
console.log("Using password from commandline...");
|
||||
return argv["pass"];
|
||||
function getPass (argv, action) {
|
||||
// Find whether we use cmdline or prompt password
|
||||
if (typeof argv['pass'] !== 'string') {
|
||||
return readline.question(`Password for ${argv[action]}:`, { hideEchoBack: true })
|
||||
}
|
||||
console.log('Using password from commandline...')
|
||||
return argv['pass']
|
||||
}
|
||||
|
||||
// Using an async function to be able to use await inside
|
||||
async function createUser(argv) {
|
||||
const existing_user = await models.User.findOne({where: {email: argv["add"]}});
|
||||
// Cannot create already-existing users
|
||||
if(existing_user != undefined) {
|
||||
console.log(`User with e-mail ${existing_user.email} already exists! Aborting ...`);
|
||||
process.exit(1);
|
||||
}
|
||||
async function createUser (argv) {
|
||||
const existingUser = await models.User.findOne({ where: { email: argv['add'] } })
|
||||
// Cannot create already-existing users
|
||||
if (existingUser !== undefined) {
|
||||
console.log(`User with e-mail ${existingUser.email} already exists! Aborting ...`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const pass = getPass(argv, "add");
|
||||
const pass = getPass(argv, 'add')
|
||||
|
||||
|
||||
// Lets try to create, and check success
|
||||
const ref = await models.User.create({email: argv["add"], password: pass});
|
||||
if(ref == undefined) {
|
||||
console.log(`Could not create user with email ${argv["add"]}`);
|
||||
process.exit(1);
|
||||
} else
|
||||
console.log(`Created user with email ${argv["add"]}`);
|
||||
// Lets try to create, and check success
|
||||
const ref = await models.User.create({ email: argv['add'], password: pass })
|
||||
if (ref === undefined) {
|
||||
console.log(`Could not create user with email ${argv['add']}`)
|
||||
process.exit(1)
|
||||
} else { console.log(`Created user with email ${argv['add']}`) }
|
||||
}
|
||||
|
||||
// Using an async function to be able to use await inside
|
||||
async function deleteUser(argv) {
|
||||
// Cannot delete non-existing users
|
||||
const existing_user = await models.User.findOne({where: {email: argv["del"]}});
|
||||
if(existing_user === undefined) {
|
||||
console.log(`User with e-mail ${argv["del"]} does not exist, cannot delete`);
|
||||
process.exit(1);
|
||||
}
|
||||
async function deleteUser (argv) {
|
||||
// Cannot delete non-existing users
|
||||
const existingUser = await models.User.findOne({ where: { email: argv['del'] } })
|
||||
if (existingUser === undefined) {
|
||||
console.log(`User with e-mail ${argv['del']} does not exist, cannot delete`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
// Sadly .destroy() does not return any success value with all
|
||||
// backends. See sequelize #4124
|
||||
await existing_user.destroy();
|
||||
console.log(`Deleted user ${argv["del"]} ...`);
|
||||
// Sadly .destroy() does not return any success value with all
|
||||
// backends. See sequelize #4124
|
||||
await existingUser.destroy()
|
||||
console.log(`Deleted user ${argv['del']} ...`)
|
||||
}
|
||||
|
||||
|
||||
// Using an async function to be able to use await inside
|
||||
async function resetUser(argv) {
|
||||
const existing_user = await models.User.findOne({where: {email: argv["reset"]}});
|
||||
// Cannot reset non-existing users
|
||||
if(existing_user == undefined) {
|
||||
console.log(`User with e-mail ${argv["reset"]} does not exist, cannot reset`);
|
||||
process.exit(1);
|
||||
}
|
||||
async function resetUser (argv) {
|
||||
const existingUser = await models.User.findOne({ where: { email: argv['reset'] } })
|
||||
// Cannot reset non-existing users
|
||||
if (existingUser === undefined) {
|
||||
console.log(`User with e-mail ${argv['reset']} does not exist, cannot reset`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const pass = getPass(argv, "reset");
|
||||
const pass = getPass(argv, 'reset')
|
||||
|
||||
// set password and save
|
||||
existing_user.password = pass;
|
||||
await existing_user.save();
|
||||
console.log(`User with email ${argv["reset"]} password has been reset`);
|
||||
// set password and save
|
||||
existingUser.password = pass
|
||||
await existingUser.save()
|
||||
console.log(`User with email ${argv['reset']} password has been reset`)
|
||||
}
|
||||
|
||||
const options = {
|
||||
add: createUser,
|
||||
del: deleteUser,
|
||||
reset: resetUser,
|
||||
};
|
||||
add: createUser,
|
||||
del: deleteUser,
|
||||
reset: resetUser
|
||||
}
|
||||
|
||||
// Perform commandline-parsing
|
||||
const argv = minimist(process.argv.slice(2));
|
||||
const argv = minimist(process.argv.slice(2))
|
||||
|
||||
const keys = Object.keys(options);
|
||||
const opts = keys.filter((key) => argv[key] !== undefined);
|
||||
const action = opts[0];
|
||||
const keys = Object.keys(options)
|
||||
const opts = keys.filter((key) => argv[key] !== undefined)
|
||||
const action = opts[0]
|
||||
|
||||
// Check for options missing
|
||||
if (opts.length === 0) {
|
||||
showUsage(`You did not specify either ${keys.map((key) => `--${key}`).join(' or ')}!`);
|
||||
showUsage(`You did not specify either ${keys.map((key) => `--${key}`).join(' or ')}!`)
|
||||
}
|
||||
|
||||
// Check if both are specified
|
||||
if (opts.length > 1) {
|
||||
showUsage(`You cannot ${action.join(' and ')} at the same time!`);
|
||||
showUsage(`You cannot ${action.join(' and ')} at the same time!`)
|
||||
}
|
||||
// Check if not string
|
||||
if (typeof argv[action] !== 'string') {
|
||||
showUsage(`You must follow an email after --${action}`);
|
||||
showUsage(`You must follow an email after --${action}`)
|
||||
}
|
||||
|
||||
// Call respective processing functions
|
||||
options[action](argv).then(function() {
|
||||
process.exit(0);
|
||||
});
|
||||
options[action](argv).then(function () {
|
||||
process.exit(0)
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict'
|
||||
|
||||
const {toBooleanConfig, toArrayConfig, toIntegerConfig} = require('./utils')
|
||||
const { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')
|
||||
|
||||
module.exports = {
|
||||
sourceURL: process.env.CMD_SOURCE_URL,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict'
|
||||
|
||||
const {toBooleanConfig, toArrayConfig, toIntegerConfig} = require('./utils')
|
||||
const { toBooleanConfig, toArrayConfig, toIntegerConfig } = require('./utils')
|
||||
|
||||
module.exports = {
|
||||
domain: process.env.HMD_DOMAIN,
|
||||
|
|
|
@ -4,11 +4,11 @@
|
|||
const crypto = require('crypto')
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const {merge} = require('lodash')
|
||||
const { merge } = require('lodash')
|
||||
const deepFreeze = require('deep-freeze')
|
||||
const {Environment, Permission} = require('./enum')
|
||||
const { Environment, Permission } = require('./enum')
|
||||
const logger = require('../logger')
|
||||
const {getGitCommit, getGitHubURL} = require('./utils')
|
||||
const { getGitCommit, getGitHubURL } = require('./utils')
|
||||
|
||||
const appRootPath = path.resolve(__dirname, '../../')
|
||||
const env = process.env.NODE_ENV || Environment.development
|
||||
|
@ -17,7 +17,7 @@ const debugConfig = {
|
|||
}
|
||||
|
||||
// Get version string from package.json
|
||||
const {version, repository} = require(path.join(appRootPath, 'package.json'))
|
||||
const { version, repository } = require(path.join(appRootPath, 'package.json'))
|
||||
|
||||
const commitID = getGitCommit(appRootPath)
|
||||
const sourceURL = getGitHubURL(repository.url, commitID || version)
|
||||
|
@ -159,8 +159,8 @@ if (Object.keys(process.env).toString().indexOf('HMD_') !== -1) {
|
|||
if (config.sessionSecret === 'secret') {
|
||||
logger.warn('Session secret not set. Using random generated one. Please set `sessionSecret` in your config.js file. All users will be logged out.')
|
||||
config.sessionSecret = crypto.randomBytes(Math.ceil(config.sessionSecretLen / 2)) // generate crypto graphic random number
|
||||
.toString('hex') // convert to hexadecimal format
|
||||
.slice(0, config.sessionSecretLen) // return required number of characters
|
||||
.toString('hex') // convert to hexadecimal format
|
||||
.slice(0, config.sessionSecretLen) // return required number of characters
|
||||
}
|
||||
|
||||
// Validate upload upload providers
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict'
|
||||
|
||||
const {toBooleanConfig} = require('./utils')
|
||||
const { toBooleanConfig } = require('./utils')
|
||||
|
||||
module.exports = {
|
||||
debug: toBooleanConfig(process.env.DEBUG),
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict'
|
||||
// history
|
||||
// external modules
|
||||
var LZString = require('lz-string')
|
||||
var LZString = require('@hackmd/lz-string')
|
||||
|
||||
// core
|
||||
var config = require('./config')
|
||||
|
|
|
@ -30,14 +30,14 @@ exports.generateAvatarURL = function (name, email = '', big = true) {
|
|||
if (typeof email !== 'string') {
|
||||
email = '' + name + '@example.com'
|
||||
}
|
||||
name=encodeURIComponent(name)
|
||||
name = encodeURIComponent(name)
|
||||
|
||||
let hash = crypto.createHash('md5')
|
||||
hash.update(email.toLowerCase())
|
||||
let hexDigest = hash.digest('hex')
|
||||
|
||||
if (email !== '' && config.allowGravatar) {
|
||||
photo = 'https://www.gravatar.com/avatar/' + hexDigest;
|
||||
photo = 'https://www.gravatar.com/avatar/' + hexDigest
|
||||
if (big) {
|
||||
photo += '?s=400'
|
||||
} else {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
'use strict'
|
||||
const {createLogger, format, transports} = require('winston')
|
||||
const { createLogger, format, transports } = require('winston')
|
||||
|
||||
const logger = createLogger({
|
||||
level: 'debug',
|
||||
|
|
|
@ -18,8 +18,8 @@ module.exports = {
|
|||
|
||||
down: function (queryInterface, Sequelize) {
|
||||
return queryInterface.removeColumn('Notes', 'lastchangeAt')
|
||||
.then(function () {
|
||||
return queryInterface.removeColumn('Notes', 'lastchangeuserId')
|
||||
})
|
||||
.then(function () {
|
||||
return queryInterface.removeColumn('Notes', 'lastchangeuserId')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
'use strict'
|
||||
module.exports = {
|
||||
up: function (queryInterface, Sequelize) {
|
||||
queryInterface.changeColumn('Notes', 'content', {type: Sequelize.TEXT('long')})
|
||||
queryInterface.changeColumn('Revisions', 'patch', {type: Sequelize.TEXT('long')})
|
||||
queryInterface.changeColumn('Revisions', 'content', {type: Sequelize.TEXT('long')})
|
||||
queryInterface.changeColumn('Revisions', 'lastContent', {type: Sequelize.TEXT('long')})
|
||||
queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT('long') })
|
||||
queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT('long') })
|
||||
queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT('long') })
|
||||
queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT('long') })
|
||||
},
|
||||
|
||||
down: function (queryInterface, Sequelize) {
|
||||
queryInterface.changeColumn('Notes', 'content', {type: Sequelize.TEXT})
|
||||
queryInterface.changeColumn('Revisions', 'patch', {type: Sequelize.TEXT})
|
||||
queryInterface.changeColumn('Revisions', 'content', {type: Sequelize.TEXT})
|
||||
queryInterface.changeColumn('Revisions', 'lastContent', {type: Sequelize.TEXT})
|
||||
queryInterface.changeColumn('Notes', 'content', { type: Sequelize.TEXT })
|
||||
queryInterface.changeColumn('Revisions', 'patch', { type: Sequelize.TEXT })
|
||||
queryInterface.changeColumn('Revisions', 'content', { type: Sequelize.TEXT })
|
||||
queryInterface.changeColumn('Revisions', 'lastContent', { type: Sequelize.TEXT })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,12 +2,12 @@
|
|||
|
||||
module.exports = {
|
||||
up: function (queryInterface, Sequelize) {
|
||||
queryInterface.changeColumn('Notes', 'authorship', {type: Sequelize.TEXT('long')})
|
||||
queryInterface.changeColumn('Revisions', 'authorship', {type: Sequelize.TEXT('long')})
|
||||
queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT('long') })
|
||||
queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT('long') })
|
||||
},
|
||||
|
||||
down: function (queryInterface, Sequelize) {
|
||||
queryInterface.changeColumn('Notes', 'authorship', {type: Sequelize.TEXT})
|
||||
queryInterface.changeColumn('Revisions', 'authorship', {type: Sequelize.TEXT})
|
||||
queryInterface.changeColumn('Notes', 'authorship', { type: Sequelize.TEXT })
|
||||
queryInterface.changeColumn('Revisions', 'authorship', { type: Sequelize.TEXT })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
module.exports = {
|
||||
up: function (queryInterface, Sequelize) {
|
||||
queryInterface.changeColumn('Notes', 'permission', {type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private')})
|
||||
queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'limited', 'locked', 'protected', 'private') })
|
||||
},
|
||||
|
||||
down: function (queryInterface, Sequelize) {
|
||||
queryInterface.changeColumn('Notes', 'permission', {type: Sequelize.ENUM('freely', 'editable', 'locked', 'private')})
|
||||
queryInterface.changeColumn('Notes', 'permission', { type: Sequelize.ENUM('freely', 'editable', 'locked', 'private') })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,25 +18,25 @@ module.exports = function (sequelize, DataTypes) {
|
|||
unique: true,
|
||||
fields: ['noteId', 'userId']
|
||||
}
|
||||
],
|
||||
classMethods: {
|
||||
associate: function (models) {
|
||||
Author.belongsTo(models.Note, {
|
||||
foreignKey: 'noteId',
|
||||
as: 'note',
|
||||
constraints: false,
|
||||
onDelete: 'CASCADE',
|
||||
hooks: true
|
||||
})
|
||||
Author.belongsTo(models.User, {
|
||||
foreignKey: 'userId',
|
||||
as: 'user',
|
||||
constraints: false,
|
||||
onDelete: 'CASCADE',
|
||||
hooks: true
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
Author.associate = function (models) {
|
||||
Author.belongsTo(models.Note, {
|
||||
foreignKey: 'noteId',
|
||||
as: 'note',
|
||||
constraints: false,
|
||||
onDelete: 'CASCADE',
|
||||
hooks: true
|
||||
})
|
||||
Author.belongsTo(models.User, {
|
||||
foreignKey: 'userId',
|
||||
as: 'user',
|
||||
constraints: false,
|
||||
onDelete: 'CASCADE',
|
||||
hooks: true
|
||||
})
|
||||
}
|
||||
|
||||
return Author
|
||||
}
|
||||
|
|
|
@ -3,14 +3,16 @@
|
|||
var fs = require('fs')
|
||||
var path = require('path')
|
||||
var Sequelize = require('sequelize')
|
||||
const {cloneDeep} = require('lodash')
|
||||
const { cloneDeep } = require('lodash')
|
||||
|
||||
// core
|
||||
var config = require('../config')
|
||||
var logger = require('../logger')
|
||||
|
||||
var dbconfig = cloneDeep(config.db)
|
||||
dbconfig.logging = config.debug ? logger.info : false
|
||||
dbconfig.logging = config.debug ? (data) => {
|
||||
logger.info(data)
|
||||
} : false
|
||||
|
||||
var sequelize = null
|
||||
|
||||
|
@ -39,13 +41,13 @@ sequelize.processData = processData
|
|||
var db = {}
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter(function (file) {
|
||||
return (file.indexOf('.') !== 0) && (file !== 'index.js')
|
||||
})
|
||||
.forEach(function (file) {
|
||||
var model = sequelize.import(path.join(__dirname, file))
|
||||
db[model.name] = model
|
||||
})
|
||||
.filter(function (file) {
|
||||
return (file.indexOf('.') !== 0) && (file !== 'index.js')
|
||||
})
|
||||
.forEach(function (file) {
|
||||
var model = sequelize.import(path.join(__dirname, file))
|
||||
db[model.name] = model
|
||||
})
|
||||
|
||||
Object.keys(db).forEach(function (modelName) {
|
||||
if ('associate' in db[modelName]) {
|
||||
|
|
|
@ -2,18 +2,19 @@
|
|||
// external modules
|
||||
var fs = require('fs')
|
||||
var path = require('path')
|
||||
var LZString = require('lz-string')
|
||||
var LZString = require('@hackmd/lz-string')
|
||||
var base64url = require('base64url')
|
||||
var md = require('markdown-it')()
|
||||
var metaMarked = require('meta-marked')
|
||||
var metaMarked = require('@hackmd/meta-marked')
|
||||
var cheerio = require('cheerio')
|
||||
var shortId = require('shortid')
|
||||
var Sequelize = require('sequelize')
|
||||
var async = require('async')
|
||||
var moment = require('moment')
|
||||
var DiffMatchPatch = require('diff-match-patch')
|
||||
var DiffMatchPatch = require('@hackmd/diff-match-patch')
|
||||
var dmp = new DiffMatchPatch()
|
||||
var S = require('string')
|
||||
|
||||
const { stripTags } = require('../../utils/string')
|
||||
|
||||
// core
|
||||
var config = require('../config')
|
||||
|
@ -86,486 +87,492 @@ module.exports = function (sequelize, DataTypes) {
|
|||
}
|
||||
}, {
|
||||
paranoid: false,
|
||||
classMethods: {
|
||||
associate: function (models) {
|
||||
Note.belongsTo(models.User, {
|
||||
foreignKey: 'ownerId',
|
||||
as: 'owner',
|
||||
constraints: false,
|
||||
onDelete: 'CASCADE',
|
||||
hooks: true
|
||||
})
|
||||
Note.belongsTo(models.User, {
|
||||
foreignKey: 'lastchangeuserId',
|
||||
as: 'lastchangeuser',
|
||||
constraints: false
|
||||
})
|
||||
Note.hasMany(models.Revision, {
|
||||
foreignKey: 'noteId',
|
||||
constraints: false
|
||||
})
|
||||
Note.hasMany(models.Author, {
|
||||
foreignKey: 'noteId',
|
||||
as: 'authors',
|
||||
constraints: false
|
||||
})
|
||||
},
|
||||
checkFileExist: function (filePath) {
|
||||
try {
|
||||
return fs.statSync(filePath).isFile()
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
},
|
||||
encodeNoteId: function (id) {
|
||||
// remove dashes in UUID and encode in url-safe base64
|
||||
let str = id.replace(/-/g, '')
|
||||
let hexStr = Buffer.from(str, 'hex')
|
||||
return base64url.encode(hexStr)
|
||||
},
|
||||
decodeNoteId: function (encodedId) {
|
||||
// decode from url-safe base64
|
||||
let id = base64url.toBuffer(encodedId).toString('hex')
|
||||
// add dashes between the UUID string parts
|
||||
let idParts = []
|
||||
idParts.push(id.substr(0, 8))
|
||||
idParts.push(id.substr(8, 4))
|
||||
idParts.push(id.substr(12, 4))
|
||||
idParts.push(id.substr(16, 4))
|
||||
idParts.push(id.substr(20, 12))
|
||||
return idParts.join('-')
|
||||
},
|
||||
checkNoteIdValid: function (id) {
|
||||
var 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
|
||||
var result = id.match(uuidRegex)
|
||||
if (result && result.length === 1) { return true } else { return false }
|
||||
},
|
||||
parseNoteId: function (noteId, callback) {
|
||||
async.series({
|
||||
parseNoteIdByAlias: function (_callback) {
|
||||
// try to parse note id by alias (e.g. doc)
|
||||
Note.findOne({
|
||||
where: {
|
||||
alias: noteId
|
||||
}
|
||||
}).then(function (note) {
|
||||
if (note) {
|
||||
let filePath = path.join(config.docsPath, noteId + '.md')
|
||||
if (Note.checkFileExist(filePath)) {
|
||||
// if doc in filesystem have newer modified time than last change time
|
||||
// then will update the doc in db
|
||||
var fsModifiedTime = moment(fs.statSync(filePath).mtime)
|
||||
var dbModifiedTime = moment(note.lastchangeAt || note.createdAt)
|
||||
var body = fs.readFileSync(filePath, 'utf8')
|
||||
var contentLength = body.length
|
||||
var title = Note.parseNoteTitle(body)
|
||||
if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) {
|
||||
note.update({
|
||||
title: title,
|
||||
content: body,
|
||||
lastchangeAt: fsModifiedTime
|
||||
}).then(function (note) {
|
||||
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
|
||||
if (err) return _callback(err, null)
|
||||
// update authorship on after making revision of docs
|
||||
var patch = dmp.patch_fromText(revision.patch)
|
||||
var operations = Note.transformPatchToOperations(patch, contentLength)
|
||||
var authorship = note.authorship
|
||||
for (let i = 0; i < operations.length; i++) {
|
||||
authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship)
|
||||
}
|
||||
note.update({
|
||||
authorship: authorship
|
||||
}).then(function (note) {
|
||||
return callback(null, note.id)
|
||||
}).catch(function (err) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
})
|
||||
}).catch(function (err) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
} else {
|
||||
return callback(null, note.id)
|
||||
}
|
||||
} else {
|
||||
return callback(null, note.id)
|
||||
}
|
||||
} else {
|
||||
var filePath = path.join(config.docsPath, noteId + '.md')
|
||||
if (Note.checkFileExist(filePath)) {
|
||||
Note.create({
|
||||
alias: noteId,
|
||||
owner: null,
|
||||
permission: 'locked'
|
||||
}).then(function (note) {
|
||||
return callback(null, note.id)
|
||||
}).catch(function (err) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
} else {
|
||||
return _callback(null, null)
|
||||
}
|
||||
}
|
||||
}).catch(function (err) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
},
|
||||
// parse note id by LZString is deprecated, here for compability
|
||||
parseNoteIdByLZString: function (_callback) {
|
||||
// Calculate minimal string length for an UUID that is encoded
|
||||
// base64 encoded and optimize comparsion by using -1
|
||||
// this should make a lot of LZ-String parsing errors obsolete
|
||||
// as we can assume that a nodeId that is 48 chars or longer is a
|
||||
// noteID.
|
||||
const base64UuidLength = ((4 * 36) / 3) - 1
|
||||
if (!(noteId.length > base64UuidLength)) {
|
||||
return _callback(null, null)
|
||||
}
|
||||
// try to parse note id by LZString Base64
|
||||
try {
|
||||
var id = LZString.decompressFromBase64(noteId)
|
||||
if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) }
|
||||
} catch (err) {
|
||||
if (err.message === 'Cannot read property \'charAt\' of undefined') {
|
||||
logger.warning('Looks like we can not decode "' + noteId + '" with LZString. Can be ignored.')
|
||||
} else {
|
||||
logger.error(err)
|
||||
}
|
||||
return _callback(null, null)
|
||||
}
|
||||
},
|
||||
parseNoteIdByBase64Url: function (_callback) {
|
||||
// try to parse note id by base64url
|
||||
try {
|
||||
var id = Note.decodeNoteId(noteId)
|
||||
if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) }
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
return _callback(null, null)
|
||||
}
|
||||
},
|
||||
parseNoteIdByShortId: function (_callback) {
|
||||
// try to parse note id by shortId
|
||||
try {
|
||||
if (shortId.isValid(noteId)) {
|
||||
Note.findOne({
|
||||
where: {
|
||||
shortid: noteId
|
||||
}
|
||||
}).then(function (note) {
|
||||
if (!note) return _callback(null, null)
|
||||
return callback(null, note.id)
|
||||
}).catch(function (err) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
} else {
|
||||
return _callback(null, null)
|
||||
}
|
||||
} catch (err) {
|
||||
return _callback(err, null)
|
||||
}
|
||||
}
|
||||
}, function (err, result) {
|
||||
if (err) {
|
||||
logger.error(err)
|
||||
return callback(err, null)
|
||||
}
|
||||
return callback(null, null)
|
||||
})
|
||||
},
|
||||
parseNoteInfo: function (body) {
|
||||
var parsed = Note.extractMeta(body)
|
||||
var $ = cheerio.load(md.render(parsed.markdown))
|
||||
return {
|
||||
title: Note.extractNoteTitle(parsed.meta, $),
|
||||
tags: Note.extractNoteTags(parsed.meta, $)
|
||||
}
|
||||
},
|
||||
parseNoteTitle: function (body) {
|
||||
var parsed = Note.extractMeta(body)
|
||||
var $ = cheerio.load(md.render(parsed.markdown))
|
||||
return Note.extractNoteTitle(parsed.meta, $)
|
||||
},
|
||||
extractNoteTitle: function (meta, $) {
|
||||
var title = ''
|
||||
if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) {
|
||||
title = meta.title
|
||||
} else {
|
||||
var h1s = $('h1')
|
||||
if (h1s.length > 0 && h1s.first().text().split('\n').length === 1) { title = S(h1s.first().text()).stripTags().s }
|
||||
}
|
||||
if (!title) title = 'Untitled'
|
||||
return title
|
||||
},
|
||||
generateDescription: function (markdown) {
|
||||
return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' ')
|
||||
},
|
||||
decodeTitle: function (title) {
|
||||
return title || 'Untitled'
|
||||
},
|
||||
generateWebTitle: function (title) {
|
||||
title = !title || title === 'Untitled' ? 'CodiMD - Collaborative markdown notes' : title + ' - CodiMD'
|
||||
return title
|
||||
},
|
||||
extractNoteTags: function (meta, $) {
|
||||
var tags = []
|
||||
var rawtags = []
|
||||
if (meta.tags && (typeof meta.tags === 'string' || typeof meta.tags === 'number')) {
|
||||
var metaTags = ('' + meta.tags).split(',')
|
||||
for (let i = 0; i < metaTags.length; i++) {
|
||||
var text = metaTags[i].trim()
|
||||
if (text) rawtags.push(text)
|
||||
}
|
||||
} else {
|
||||
var h6s = $('h6')
|
||||
h6s.each(function (key, value) {
|
||||
if (/^tags/gmi.test($(value).text())) {
|
||||
var codes = $(value).find('code')
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
var text = S($(codes[i]).text().trim()).stripTags().s
|
||||
if (text) rawtags.push(text)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
for (let i = 0; i < rawtags.length; i++) {
|
||||
var found = false
|
||||
for (let j = 0; j < tags.length; j++) {
|
||||
if (tags[j] === rawtags[i]) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!found) { tags.push(rawtags[i]) }
|
||||
}
|
||||
return tags
|
||||
},
|
||||
extractMeta: function (content) {
|
||||
var obj = null
|
||||
try {
|
||||
obj = metaMarked(content)
|
||||
if (!obj.markdown) obj.markdown = ''
|
||||
if (!obj.meta) obj.meta = {}
|
||||
} catch (err) {
|
||||
obj = {
|
||||
markdown: content,
|
||||
meta: {}
|
||||
}
|
||||
}
|
||||
return obj
|
||||
},
|
||||
parseMeta: function (meta) {
|
||||
var _meta = {}
|
||||
if (meta) {
|
||||
if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { _meta.title = meta.title }
|
||||
if (meta.description && (typeof meta.description === 'string' || typeof meta.description === 'number')) { _meta.description = meta.description }
|
||||
if (meta.robots && (typeof meta.robots === 'string' || typeof meta.robots === 'number')) { _meta.robots = meta.robots }
|
||||
if (meta.GA && (typeof meta.GA === 'string' || typeof meta.GA === 'number')) { _meta.GA = meta.GA }
|
||||
if (meta.disqus && (typeof meta.disqus === 'string' || typeof meta.disqus === 'number')) { _meta.disqus = meta.disqus }
|
||||
if (meta.slideOptions && (typeof meta.slideOptions === 'object')) { _meta.slideOptions = meta.slideOptions }
|
||||
}
|
||||
return _meta
|
||||
},
|
||||
updateAuthorshipByOperation: function (operation, userId, authorships) {
|
||||
var index = 0
|
||||
var timestamp = Date.now()
|
||||
for (let i = 0; i < operation.length; i++) {
|
||||
var op = operation[i]
|
||||
if (ot.TextOperation.isRetain(op)) {
|
||||
index += op
|
||||
} else if (ot.TextOperation.isInsert(op)) {
|
||||
let opStart = index
|
||||
let opEnd = index + op.length
|
||||
var inserted = false
|
||||
// authorship format: [userId, startPos, endPos, createdAt, updatedAt]
|
||||
if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp])
|
||||
else {
|
||||
for (let j = 0; j < authorships.length; j++) {
|
||||
let authorship = authorships[j]
|
||||
if (!inserted) {
|
||||
let nextAuthorship = authorships[j + 1] || -1
|
||||
if ((nextAuthorship !== -1 && nextAuthorship[1] >= opEnd) || j >= authorships.length - 1) {
|
||||
if (authorship[1] < opStart && authorship[2] > opStart) {
|
||||
// divide
|
||||
let postLength = authorship[2] - opStart
|
||||
authorship[2] = opStart
|
||||
authorship[4] = timestamp
|
||||
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp])
|
||||
authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp])
|
||||
j += 2
|
||||
inserted = true
|
||||
} else if (authorship[1] >= opStart) {
|
||||
authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp])
|
||||
j += 1
|
||||
inserted = true
|
||||
} else if (authorship[2] <= opStart) {
|
||||
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp])
|
||||
j += 1
|
||||
inserted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (authorship[1] >= opStart) {
|
||||
authorship[1] += op.length
|
||||
authorship[2] += op.length
|
||||
}
|
||||
}
|
||||
}
|
||||
index += op.length
|
||||
} else if (ot.TextOperation.isDelete(op)) {
|
||||
let opStart = index
|
||||
let opEnd = index - op
|
||||
if (operation.length === 1) {
|
||||
authorships = []
|
||||
} else if (authorships.length > 0) {
|
||||
for (let j = 0; j < authorships.length; j++) {
|
||||
let authorship = authorships[j]
|
||||
if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) {
|
||||
authorships.splice(j, 1)
|
||||
j -= 1
|
||||
} else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) {
|
||||
authorship[2] += op
|
||||
authorship[4] = timestamp
|
||||
} else if (authorship[2] >= opStart && authorship[2] <= opEnd) {
|
||||
authorship[2] = opStart
|
||||
authorship[4] = timestamp
|
||||
} else if (authorship[1] >= opStart && authorship[1] <= opEnd) {
|
||||
authorship[1] = opEnd
|
||||
authorship[4] = timestamp
|
||||
}
|
||||
if (authorship[1] >= opEnd) {
|
||||
authorship[1] += op
|
||||
authorship[2] += op
|
||||
}
|
||||
}
|
||||
}
|
||||
index += op
|
||||
}
|
||||
}
|
||||
// merge
|
||||
for (let j = 0; j < authorships.length; j++) {
|
||||
let authorship = authorships[j]
|
||||
for (let k = j + 1; k < authorships.length; k++) {
|
||||
let nextAuthorship = authorships[k]
|
||||
if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) {
|
||||
let minTimestamp = Math.min(authorship[3], nextAuthorship[3])
|
||||
let maxTimestamp = Math.max(authorship[3], nextAuthorship[3])
|
||||
authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp])
|
||||
authorships.splice(k, 1)
|
||||
j -= 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// clear
|
||||
for (let j = 0; j < authorships.length; j++) {
|
||||
let authorship = authorships[j]
|
||||
if (!authorship[0]) {
|
||||
authorships.splice(j, 1)
|
||||
j -= 1
|
||||
}
|
||||
}
|
||||
return authorships
|
||||
},
|
||||
transformPatchToOperations: function (patch, contentLength) {
|
||||
var operations = []
|
||||
if (patch.length > 0) {
|
||||
// calculate original content length
|
||||
for (let j = patch.length - 1; j >= 0; j--) {
|
||||
var p = patch[j]
|
||||
for (let i = 0; i < p.diffs.length; i++) {
|
||||
var diff = p.diffs[i]
|
||||
switch (diff[0]) {
|
||||
case 1: // insert
|
||||
contentLength -= diff[1].length
|
||||
break
|
||||
case -1: // delete
|
||||
contentLength += diff[1].length
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// generate operations
|
||||
var bias = 0
|
||||
var lengthBias = 0
|
||||
for (let j = 0; j < patch.length; j++) {
|
||||
var operation = []
|
||||
let p = patch[j]
|
||||
var currIndex = p.start1
|
||||
var currLength = contentLength - bias
|
||||
for (let i = 0; i < p.diffs.length; i++) {
|
||||
let diff = p.diffs[i]
|
||||
switch (diff[0]) {
|
||||
case 0: // retain
|
||||
if (i === 0) {
|
||||
// first
|
||||
operation.push(currIndex + diff[1].length)
|
||||
} else if (i !== p.diffs.length - 1) {
|
||||
// mid
|
||||
operation.push(diff[1].length)
|
||||
} else {
|
||||
// last
|
||||
operation.push(currLength + lengthBias - currIndex)
|
||||
}
|
||||
currIndex += diff[1].length
|
||||
break
|
||||
case 1: // insert
|
||||
operation.push(diff[1])
|
||||
lengthBias += diff[1].length
|
||||
currIndex += diff[1].length
|
||||
break
|
||||
case -1: // delete
|
||||
operation.push(-diff[1].length)
|
||||
bias += diff[1].length
|
||||
currIndex += diff[1].length
|
||||
break
|
||||
}
|
||||
}
|
||||
operations.push(operation)
|
||||
}
|
||||
}
|
||||
return operations
|
||||
}
|
||||
},
|
||||
hooks: {
|
||||
beforeCreate: function (note, options, callback) {
|
||||
// if no content specified then use default note
|
||||
if (!note.content) {
|
||||
var body = null
|
||||
let filePath = null
|
||||
if (!note.alias) {
|
||||
filePath = config.defaultNotePath
|
||||
} else {
|
||||
filePath = path.join(config.docsPath, note.alias + '.md')
|
||||
}
|
||||
if (Note.checkFileExist(filePath)) {
|
||||
var fsCreatedTime = moment(fs.statSync(filePath).ctime)
|
||||
body = fs.readFileSync(filePath, 'utf8')
|
||||
note.title = Note.parseNoteTitle(body)
|
||||
note.content = body
|
||||
if (filePath !== config.defaultNotePath) {
|
||||
note.createdAt = fsCreatedTime
|
||||
beforeCreate: function (note, options) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
// if no content specified then use default note
|
||||
if (!note.content) {
|
||||
var body = null
|
||||
let filePath = null
|
||||
if (!note.alias) {
|
||||
filePath = config.defaultNotePath
|
||||
} else {
|
||||
filePath = path.join(config.docsPath, note.alias + '.md')
|
||||
}
|
||||
if (Note.checkFileExist(filePath)) {
|
||||
var fsCreatedTime = moment(fs.statSync(filePath).ctime)
|
||||
body = fs.readFileSync(filePath, 'utf8')
|
||||
note.title = Note.parseNoteTitle(body)
|
||||
note.content = body
|
||||
if (filePath !== config.defaultNotePath) {
|
||||
note.createdAt = fsCreatedTime
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// if no permission specified and have owner then give default permission in config, else default permission is freely
|
||||
if (!note.permission) {
|
||||
if (note.ownerId) {
|
||||
note.permission = config.defaultPermission
|
||||
} else {
|
||||
note.permission = 'freely'
|
||||
// if no permission specified and have owner then give default permission in config, else default permission is freely
|
||||
if (!note.permission) {
|
||||
if (note.ownerId) {
|
||||
note.permission = config.defaultPermission
|
||||
} else {
|
||||
note.permission = 'freely'
|
||||
}
|
||||
}
|
||||
}
|
||||
return callback(null, note)
|
||||
return resolve(note)
|
||||
})
|
||||
},
|
||||
afterCreate: function (note, options, callback) {
|
||||
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
|
||||
callback(err, note)
|
||||
return new Promise(function (resolve, reject) {
|
||||
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
return resolve(note)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Note.associate = function (models) {
|
||||
Note.belongsTo(models.User, {
|
||||
foreignKey: 'ownerId',
|
||||
as: 'owner',
|
||||
constraints: false,
|
||||
onDelete: 'CASCADE',
|
||||
hooks: true
|
||||
})
|
||||
Note.belongsTo(models.User, {
|
||||
foreignKey: 'lastchangeuserId',
|
||||
as: 'lastchangeuser',
|
||||
constraints: false
|
||||
})
|
||||
Note.hasMany(models.Revision, {
|
||||
foreignKey: 'noteId',
|
||||
constraints: false
|
||||
})
|
||||
Note.hasMany(models.Author, {
|
||||
foreignKey: 'noteId',
|
||||
as: 'authors',
|
||||
constraints: false
|
||||
})
|
||||
}
|
||||
Note.checkFileExist = function (filePath) {
|
||||
try {
|
||||
return fs.statSync(filePath).isFile()
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
Note.encodeNoteId = function (id) {
|
||||
// remove dashes in UUID and encode in url-safe base64
|
||||
let str = id.replace(/-/g, '')
|
||||
let hexStr = Buffer.from(str, 'hex')
|
||||
return base64url.encode(hexStr)
|
||||
}
|
||||
Note.decodeNoteId = function (encodedId) {
|
||||
// decode from url-safe base64
|
||||
let id = base64url.toBuffer(encodedId).toString('hex')
|
||||
// add dashes between the UUID string parts
|
||||
let idParts = []
|
||||
idParts.push(id.substr(0, 8))
|
||||
idParts.push(id.substr(8, 4))
|
||||
idParts.push(id.substr(12, 4))
|
||||
idParts.push(id.substr(16, 4))
|
||||
idParts.push(id.substr(20, 12))
|
||||
return idParts.join('-')
|
||||
}
|
||||
Note.checkNoteIdValid = function (id) {
|
||||
var 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
|
||||
var result = id.match(uuidRegex)
|
||||
if (result && result.length === 1) { return true } else { return false }
|
||||
}
|
||||
Note.parseNoteId = function (noteId, callback) {
|
||||
async.series({
|
||||
parseNoteIdByAlias: function (_callback) {
|
||||
// try to parse note id by alias (e.g. doc)
|
||||
Note.findOne({
|
||||
where: {
|
||||
alias: noteId
|
||||
}
|
||||
}).then(function (note) {
|
||||
if (note) {
|
||||
let filePath = path.join(config.docsPath, noteId + '.md')
|
||||
if (Note.checkFileExist(filePath)) {
|
||||
// if doc in filesystem have newer modified time than last change time
|
||||
// then will update the doc in db
|
||||
var fsModifiedTime = moment(fs.statSync(filePath).mtime)
|
||||
var dbModifiedTime = moment(note.lastchangeAt || note.createdAt)
|
||||
var body = fs.readFileSync(filePath, 'utf8')
|
||||
var contentLength = body.length
|
||||
var title = Note.parseNoteTitle(body)
|
||||
if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) {
|
||||
note.update({
|
||||
title: title,
|
||||
content: body,
|
||||
lastchangeAt: fsModifiedTime
|
||||
}).then(function (note) {
|
||||
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
|
||||
if (err) return _callback(err, null)
|
||||
// update authorship on after making revision of docs
|
||||
var patch = dmp.patch_fromText(revision.patch)
|
||||
var operations = Note.transformPatchToOperations(patch, contentLength)
|
||||
var authorship = note.authorship
|
||||
for (let i = 0; i < operations.length; i++) {
|
||||
authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship)
|
||||
}
|
||||
note.update({
|
||||
authorship: authorship
|
||||
}).then(function (note) {
|
||||
return callback(null, note.id)
|
||||
}).catch(function (err) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
})
|
||||
}).catch(function (err) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
} else {
|
||||
return callback(null, note.id)
|
||||
}
|
||||
} else {
|
||||
return callback(null, note.id)
|
||||
}
|
||||
} else {
|
||||
var filePath = path.join(config.docsPath, noteId + '.md')
|
||||
if (Note.checkFileExist(filePath)) {
|
||||
Note.create({
|
||||
alias: noteId,
|
||||
owner: null,
|
||||
permission: 'locked'
|
||||
}).then(function (note) {
|
||||
return callback(null, note.id)
|
||||
}).catch(function (err) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
} else {
|
||||
return _callback(null, null)
|
||||
}
|
||||
}
|
||||
}).catch(function (err) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
},
|
||||
// parse note id by LZString is deprecated, here for compability
|
||||
parseNoteIdByLZString: function (_callback) {
|
||||
// Calculate minimal string length for an UUID that is encoded
|
||||
// base64 encoded and optimize comparsion by using -1
|
||||
// this should make a lot of LZ-String parsing errors obsolete
|
||||
// as we can assume that a nodeId that is 48 chars or longer is a
|
||||
// noteID.
|
||||
const base64UuidLength = ((4 * 36) / 3) - 1
|
||||
if (!(noteId.length > base64UuidLength)) {
|
||||
return _callback(null, null)
|
||||
}
|
||||
// try to parse note id by LZString Base64
|
||||
try {
|
||||
var id = LZString.decompressFromBase64(noteId)
|
||||
if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) }
|
||||
} catch (err) {
|
||||
if (err.message === 'Cannot read property \'charAt\' of undefined') {
|
||||
logger.warning('Looks like we can not decode "' + noteId + '" with LZString. Can be ignored.')
|
||||
} else {
|
||||
logger.error(err)
|
||||
}
|
||||
return _callback(null, null)
|
||||
}
|
||||
},
|
||||
parseNoteIdByBase64Url: function (_callback) {
|
||||
// try to parse note id by base64url
|
||||
try {
|
||||
var id = Note.decodeNoteId(noteId)
|
||||
if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) }
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
return _callback(null, null)
|
||||
}
|
||||
},
|
||||
parseNoteIdByShortId: function (_callback) {
|
||||
// try to parse note id by shortId
|
||||
try {
|
||||
if (shortId.isValid(noteId)) {
|
||||
Note.findOne({
|
||||
where: {
|
||||
shortid: noteId
|
||||
}
|
||||
}).then(function (note) {
|
||||
if (!note) return _callback(null, null)
|
||||
return callback(null, note.id)
|
||||
}).catch(function (err) {
|
||||
return _callback(err, null)
|
||||
})
|
||||
} else {
|
||||
return _callback(null, null)
|
||||
}
|
||||
} catch (err) {
|
||||
return _callback(err, null)
|
||||
}
|
||||
}
|
||||
}, function (err, result) {
|
||||
if (err) {
|
||||
logger.error(err)
|
||||
return callback(err, null)
|
||||
}
|
||||
return callback(null, null)
|
||||
})
|
||||
}
|
||||
Note.parseNoteInfo = function (body) {
|
||||
var parsed = Note.extractMeta(body)
|
||||
var $ = cheerio.load(md.render(parsed.markdown))
|
||||
return {
|
||||
title: Note.extractNoteTitle(parsed.meta, $),
|
||||
tags: Note.extractNoteTags(parsed.meta, $)
|
||||
}
|
||||
}
|
||||
Note.parseNoteTitle = function (body) {
|
||||
var parsed = Note.extractMeta(body)
|
||||
var $ = cheerio.load(md.render(parsed.markdown))
|
||||
return Note.extractNoteTitle(parsed.meta, $)
|
||||
}
|
||||
Note.extractNoteTitle = function (meta, $) {
|
||||
var title = ''
|
||||
if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) {
|
||||
title = meta.title
|
||||
} else {
|
||||
var h1s = $('h1')
|
||||
if (h1s.length > 0 && h1s.first().text().split('\n').length === 1) { title = stripTags(h1s.first().text()) }
|
||||
}
|
||||
if (!title) title = 'Untitled'
|
||||
return title
|
||||
}
|
||||
Note.generateDescription = function (markdown) {
|
||||
return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' ')
|
||||
}
|
||||
Note.decodeTitle = function (title) {
|
||||
return title || 'Untitled'
|
||||
}
|
||||
Note.generateWebTitle = function (title) {
|
||||
title = !title || title === 'Untitled' ? 'CodiMD - Collaborative markdown notes' : title + ' - CodiMD'
|
||||
return title
|
||||
}
|
||||
Note.extractNoteTags = function (meta, $) {
|
||||
var tags = []
|
||||
var rawtags = []
|
||||
if (meta.tags && (typeof meta.tags === 'string' || typeof meta.tags === 'number')) {
|
||||
var metaTags = ('' + meta.tags).split(',')
|
||||
for (let i = 0; i < metaTags.length; i++) {
|
||||
var text = metaTags[i].trim()
|
||||
if (text) rawtags.push(text)
|
||||
}
|
||||
} else {
|
||||
var h6s = $('h6')
|
||||
h6s.each(function (key, value) {
|
||||
if (/^tags/gmi.test($(value).text())) {
|
||||
var codes = $(value).find('code')
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
var text = stripTags($(codes[i]).text().trim())
|
||||
if (text) rawtags.push(text)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
for (let i = 0; i < rawtags.length; i++) {
|
||||
var found = false
|
||||
for (let j = 0; j < tags.length; j++) {
|
||||
if (tags[j] === rawtags[i]) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!found) { tags.push(rawtags[i]) }
|
||||
}
|
||||
return tags
|
||||
}
|
||||
Note.extractMeta = function (content) {
|
||||
var obj = null
|
||||
try {
|
||||
obj = metaMarked(content)
|
||||
if (!obj.markdown) obj.markdown = ''
|
||||
if (!obj.meta) obj.meta = {}
|
||||
} catch (err) {
|
||||
obj = {
|
||||
markdown: content,
|
||||
meta: {}
|
||||
}
|
||||
}
|
||||
return obj
|
||||
}
|
||||
Note.parseMeta = function (meta) {
|
||||
var _meta = {}
|
||||
if (meta) {
|
||||
if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { _meta.title = meta.title }
|
||||
if (meta.description && (typeof meta.description === 'string' || typeof meta.description === 'number')) { _meta.description = meta.description }
|
||||
if (meta.robots && (typeof meta.robots === 'string' || typeof meta.robots === 'number')) { _meta.robots = meta.robots }
|
||||
if (meta.GA && (typeof meta.GA === 'string' || typeof meta.GA === 'number')) { _meta.GA = meta.GA }
|
||||
if (meta.disqus && (typeof meta.disqus === 'string' || typeof meta.disqus === 'number')) { _meta.disqus = meta.disqus }
|
||||
if (meta.slideOptions && (typeof meta.slideOptions === 'object')) { _meta.slideOptions = meta.slideOptions }
|
||||
}
|
||||
return _meta
|
||||
}
|
||||
Note.updateAuthorshipByOperation = function (operation, userId, authorships) {
|
||||
var index = 0
|
||||
var timestamp = Date.now()
|
||||
for (let i = 0; i < operation.length; i++) {
|
||||
var op = operation[i]
|
||||
if (ot.TextOperation.isRetain(op)) {
|
||||
index += op
|
||||
} else if (ot.TextOperation.isInsert(op)) {
|
||||
let opStart = index
|
||||
let opEnd = index + op.length
|
||||
var inserted = false
|
||||
// authorship format: [userId, startPos, endPos, createdAt, updatedAt]
|
||||
if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp])
|
||||
else {
|
||||
for (let j = 0; j < authorships.length; j++) {
|
||||
let authorship = authorships[j]
|
||||
if (!inserted) {
|
||||
let nextAuthorship = authorships[j + 1] || -1
|
||||
if ((nextAuthorship !== -1 && nextAuthorship[1] >= opEnd) || j >= authorships.length - 1) {
|
||||
if (authorship[1] < opStart && authorship[2] > opStart) {
|
||||
// divide
|
||||
let postLength = authorship[2] - opStart
|
||||
authorship[2] = opStart
|
||||
authorship[4] = timestamp
|
||||
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp])
|
||||
authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp])
|
||||
j += 2
|
||||
inserted = true
|
||||
} else if (authorship[1] >= opStart) {
|
||||
authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp])
|
||||
j += 1
|
||||
inserted = true
|
||||
} else if (authorship[2] <= opStart) {
|
||||
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp])
|
||||
j += 1
|
||||
inserted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if (authorship[1] >= opStart) {
|
||||
authorship[1] += op.length
|
||||
authorship[2] += op.length
|
||||
}
|
||||
}
|
||||
}
|
||||
index += op.length
|
||||
} else if (ot.TextOperation.isDelete(op)) {
|
||||
let opStart = index
|
||||
let opEnd = index - op
|
||||
if (operation.length === 1) {
|
||||
authorships = []
|
||||
} else if (authorships.length > 0) {
|
||||
for (let j = 0; j < authorships.length; j++) {
|
||||
let authorship = authorships[j]
|
||||
if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) {
|
||||
authorships.splice(j, 1)
|
||||
j -= 1
|
||||
} else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) {
|
||||
authorship[2] += op
|
||||
authorship[4] = timestamp
|
||||
} else if (authorship[2] >= opStart && authorship[2] <= opEnd) {
|
||||
authorship[2] = opStart
|
||||
authorship[4] = timestamp
|
||||
} else if (authorship[1] >= opStart && authorship[1] <= opEnd) {
|
||||
authorship[1] = opEnd
|
||||
authorship[4] = timestamp
|
||||
}
|
||||
if (authorship[1] >= opEnd) {
|
||||
authorship[1] += op
|
||||
authorship[2] += op
|
||||
}
|
||||
}
|
||||
}
|
||||
index += op
|
||||
}
|
||||
}
|
||||
// merge
|
||||
for (let j = 0; j < authorships.length; j++) {
|
||||
let authorship = authorships[j]
|
||||
for (let k = j + 1; k < authorships.length; k++) {
|
||||
let nextAuthorship = authorships[k]
|
||||
if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) {
|
||||
let minTimestamp = Math.min(authorship[3], nextAuthorship[3])
|
||||
let maxTimestamp = Math.max(authorship[3], nextAuthorship[3])
|
||||
authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp])
|
||||
authorships.splice(k, 1)
|
||||
j -= 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// clear
|
||||
for (let j = 0; j < authorships.length; j++) {
|
||||
let authorship = authorships[j]
|
||||
if (!authorship[0]) {
|
||||
authorships.splice(j, 1)
|
||||
j -= 1
|
||||
}
|
||||
}
|
||||
return authorships
|
||||
}
|
||||
Note.transformPatchToOperations = function (patch, contentLength) {
|
||||
var operations = []
|
||||
if (patch.length > 0) {
|
||||
// calculate original content length
|
||||
for (let j = patch.length - 1; j >= 0; j--) {
|
||||
var p = patch[j]
|
||||
for (let i = 0; i < p.diffs.length; i++) {
|
||||
var diff = p.diffs[i]
|
||||
switch (diff[0]) {
|
||||
case 1: // insert
|
||||
contentLength -= diff[1].length
|
||||
break
|
||||
case -1: // delete
|
||||
contentLength += diff[1].length
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// generate operations
|
||||
var bias = 0
|
||||
var lengthBias = 0
|
||||
for (let j = 0; j < patch.length; j++) {
|
||||
var operation = []
|
||||
let p = patch[j]
|
||||
var currIndex = p.start1
|
||||
var currLength = contentLength - bias
|
||||
for (let i = 0; i < p.diffs.length; i++) {
|
||||
let diff = p.diffs[i]
|
||||
switch (diff[0]) {
|
||||
case 0: // retain
|
||||
if (i === 0) {
|
||||
// first
|
||||
operation.push(currIndex + diff[1].length)
|
||||
} else if (i !== p.diffs.length - 1) {
|
||||
// mid
|
||||
operation.push(diff[1].length)
|
||||
} else {
|
||||
// last
|
||||
operation.push(currLength + lengthBias - currIndex)
|
||||
}
|
||||
currIndex += diff[1].length
|
||||
break
|
||||
case 1: // insert
|
||||
operation.push(diff[1])
|
||||
lengthBias += diff[1].length
|
||||
currIndex += diff[1].length
|
||||
break
|
||||
case -1: // delete
|
||||
operation.push(-diff[1].length)
|
||||
bias += diff[1].length
|
||||
currIndex += diff[1].length
|
||||
break
|
||||
}
|
||||
}
|
||||
operations.push(operation)
|
||||
}
|
||||
}
|
||||
return operations
|
||||
}
|
||||
|
||||
return Note
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ var childProcess = require('child_process')
|
|||
var shortId = require('shortid')
|
||||
var path = require('path')
|
||||
|
||||
var Op = Sequelize.Op
|
||||
|
||||
// core
|
||||
var config = require('../config')
|
||||
var logger = require('../logger')
|
||||
|
@ -97,214 +99,212 @@ module.exports = function (sequelize, DataTypes) {
|
|||
this.setDataValue('authorship', value ? JSON.stringify(value) : value)
|
||||
}
|
||||
}
|
||||
}, {
|
||||
classMethods: {
|
||||
associate: function (models) {
|
||||
Revision.belongsTo(models.Note, {
|
||||
foreignKey: 'noteId',
|
||||
as: 'note',
|
||||
constraints: false,
|
||||
onDelete: 'CASCADE',
|
||||
hooks: true
|
||||
})
|
||||
})
|
||||
|
||||
Revision.associate = function (models) {
|
||||
Revision.belongsTo(models.Note, {
|
||||
foreignKey: 'noteId',
|
||||
as: 'note',
|
||||
constraints: false,
|
||||
onDelete: 'CASCADE',
|
||||
hooks: true
|
||||
})
|
||||
}
|
||||
Revision.getNoteRevisions = function (note, callback) {
|
||||
Revision.findAll({
|
||||
where: {
|
||||
noteId: note.id
|
||||
},
|
||||
getNoteRevisions: function (note, callback) {
|
||||
Revision.findAll({
|
||||
where: {
|
||||
noteId: note.id
|
||||
},
|
||||
order: [['createdAt', 'DESC']]
|
||||
}).then(function (revisions) {
|
||||
var data = []
|
||||
for (var i = 0, l = revisions.length; i < l; i++) {
|
||||
var revision = revisions[i]
|
||||
data.push({
|
||||
time: moment(revision.createdAt).valueOf(),
|
||||
length: revision.length
|
||||
})
|
||||
order: [['createdAt', 'DESC']]
|
||||
}).then(function (revisions) {
|
||||
var data = []
|
||||
for (var i = 0, l = revisions.length; i < l; i++) {
|
||||
var revision = revisions[i]
|
||||
data.push({
|
||||
time: moment(revision.createdAt).valueOf(),
|
||||
length: revision.length
|
||||
})
|
||||
}
|
||||
callback(null, data)
|
||||
}).catch(function (err) {
|
||||
callback(err, null)
|
||||
})
|
||||
}
|
||||
Revision.getPatchedNoteRevisionByTime = function (note, time, callback) {
|
||||
// find all revisions to prepare for all possible calculation
|
||||
Revision.findAll({
|
||||
where: {
|
||||
noteId: note.id
|
||||
},
|
||||
order: [['createdAt', 'DESC']]
|
||||
}).then(function (revisions) {
|
||||
if (revisions.length <= 0) return callback(null, null)
|
||||
// measure target revision position
|
||||
Revision.count({
|
||||
where: {
|
||||
noteId: note.id,
|
||||
createdAt: {
|
||||
[Op.gte]: time
|
||||
}
|
||||
callback(null, data)
|
||||
}).catch(function (err) {
|
||||
callback(err, null)
|
||||
})
|
||||
},
|
||||
getPatchedNoteRevisionByTime: function (note, time, callback) {
|
||||
// find all revisions to prepare for all possible calculation
|
||||
Revision.findAll({
|
||||
where: {
|
||||
noteId: note.id
|
||||
},
|
||||
order: [['createdAt', 'DESC']]
|
||||
}).then(function (revisions) {
|
||||
if (revisions.length <= 0) return callback(null, null)
|
||||
// measure target revision position
|
||||
Revision.count({
|
||||
where: {
|
||||
noteId: note.id,
|
||||
createdAt: {
|
||||
$gte: time
|
||||
},
|
||||
order: [['createdAt', 'DESC']]
|
||||
}).then(function (count) {
|
||||
if (count <= 0) return callback(null, null)
|
||||
sendDmpWorker({
|
||||
msg: 'get revision',
|
||||
revisions: revisions,
|
||||
count: count
|
||||
}, callback)
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
Revision.checkAllNotesRevision = function (callback) {
|
||||
Revision.saveAllNotesRevision(function (err, notes) {
|
||||
if (err) return callback(err, null)
|
||||
if (!notes || notes.length <= 0) {
|
||||
return callback(null, notes)
|
||||
} else {
|
||||
Revision.checkAllNotesRevision(callback)
|
||||
}
|
||||
})
|
||||
}
|
||||
Revision.saveAllNotesRevision = function (callback) {
|
||||
sequelize.models.Note.findAll({
|
||||
// query all notes that need to save for revision
|
||||
where: {
|
||||
[Op.and]: [
|
||||
{
|
||||
lastchangeAt: {
|
||||
[Op.or]: {
|
||||
[Op.eq]: null,
|
||||
[Op.and]: {
|
||||
[Op.ne]: null,
|
||||
[Op.gt]: sequelize.col('createdAt')
|
||||
}
|
||||
}
|
||||
},
|
||||
order: [['createdAt', 'DESC']]
|
||||
}).then(function (count) {
|
||||
if (count <= 0) return callback(null, null)
|
||||
sendDmpWorker({
|
||||
msg: 'get revision',
|
||||
revisions: revisions,
|
||||
count: count
|
||||
}, callback)
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
},
|
||||
checkAllNotesRevision: function (callback) {
|
||||
Revision.saveAllNotesRevision(function (err, notes) {
|
||||
if (err) return callback(err, null)
|
||||
if (!notes || notes.length <= 0) {
|
||||
return callback(null, notes)
|
||||
}
|
||||
},
|
||||
{
|
||||
savedAt: {
|
||||
[Op.or]: {
|
||||
[Op.eq]: null,
|
||||
[Op.lt]: sequelize.col('lastchangeAt')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}).then(function (notes) {
|
||||
if (notes.length <= 0) return callback(null, notes)
|
||||
var savedNotes = []
|
||||
async.each(notes, function (note, _callback) {
|
||||
// revision saving policy: note not been modified for 5 mins or not save for 10 mins
|
||||
if (note.lastchangeAt && note.savedAt) {
|
||||
var lastchangeAt = moment(note.lastchangeAt)
|
||||
var savedAt = moment(note.savedAt)
|
||||
if (moment().isAfter(lastchangeAt.add(5, 'minutes'))) {
|
||||
savedNotes.push(note)
|
||||
Revision.saveNoteRevision(note, _callback)
|
||||
} else if (lastchangeAt.isAfter(savedAt.add(10, 'minutes'))) {
|
||||
savedNotes.push(note)
|
||||
Revision.saveNoteRevision(note, _callback)
|
||||
} else {
|
||||
Revision.checkAllNotesRevision(callback)
|
||||
return _callback(null, null)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
savedNotes.push(note)
|
||||
Revision.saveNoteRevision(note, _callback)
|
||||
}
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return callback(err, null)
|
||||
}
|
||||
// return null when no notes need saving at this moment but have delayed tasks to be done
|
||||
var result = ((savedNotes.length === 0) && (notes.length > savedNotes.length)) ? null : savedNotes
|
||||
return callback(null, result)
|
||||
})
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
Revision.saveNoteRevision = function (note, callback) {
|
||||
Revision.findAll({
|
||||
where: {
|
||||
noteId: note.id
|
||||
},
|
||||
saveAllNotesRevision: function (callback) {
|
||||
sequelize.models.Note.findAll({
|
||||
// query all notes that need to save for revision
|
||||
where: {
|
||||
$and: [
|
||||
{
|
||||
lastchangeAt: {
|
||||
$or: {
|
||||
$eq: null,
|
||||
$and: {
|
||||
$ne: null,
|
||||
$gt: sequelize.col('createdAt')
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
savedAt: {
|
||||
$or: {
|
||||
$eq: null,
|
||||
$lt: sequelize.col('lastchangeAt')
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}).then(function (notes) {
|
||||
if (notes.length <= 0) return callback(null, notes)
|
||||
var savedNotes = []
|
||||
async.each(notes, function (note, _callback) {
|
||||
// revision saving policy: note not been modified for 5 mins or not save for 10 mins
|
||||
if (note.lastchangeAt && note.savedAt) {
|
||||
var lastchangeAt = moment(note.lastchangeAt)
|
||||
var savedAt = moment(note.savedAt)
|
||||
if (moment().isAfter(lastchangeAt.add(5, 'minutes'))) {
|
||||
savedNotes.push(note)
|
||||
Revision.saveNoteRevision(note, _callback)
|
||||
} else if (lastchangeAt.isAfter(savedAt.add(10, 'minutes'))) {
|
||||
savedNotes.push(note)
|
||||
Revision.saveNoteRevision(note, _callback)
|
||||
} else {
|
||||
return _callback(null, null)
|
||||
}
|
||||
} else {
|
||||
savedNotes.push(note)
|
||||
Revision.saveNoteRevision(note, _callback)
|
||||
}
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return callback(err, null)
|
||||
}
|
||||
// return null when no notes need saving at this moment but have delayed tasks to be done
|
||||
var result = ((savedNotes.length === 0) && (notes.length > savedNotes.length)) ? null : savedNotes
|
||||
return callback(null, result)
|
||||
})
|
||||
order: [['createdAt', 'DESC']]
|
||||
}).then(function (revisions) {
|
||||
if (revisions.length <= 0) {
|
||||
// if no revision available
|
||||
Revision.create({
|
||||
noteId: note.id,
|
||||
lastContent: note.content ? note.content : '',
|
||||
length: note.content ? note.content.length : 0,
|
||||
authorship: note.authorship
|
||||
}).then(function (revision) {
|
||||
Revision.finishSaveNoteRevision(note, revision, callback)
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
},
|
||||
saveNoteRevision: function (note, callback) {
|
||||
Revision.findAll({
|
||||
where: {
|
||||
noteId: note.id
|
||||
},
|
||||
order: [['createdAt', 'DESC']]
|
||||
}).then(function (revisions) {
|
||||
if (revisions.length <= 0) {
|
||||
// if no revision available
|
||||
Revision.create({
|
||||
noteId: note.id,
|
||||
lastContent: note.content ? note.content : '',
|
||||
length: note.content ? note.content.length : 0,
|
||||
authorship: note.authorship
|
||||
} else {
|
||||
var latestRevision = revisions[0]
|
||||
var lastContent = latestRevision.content || latestRevision.lastContent
|
||||
var content = note.content
|
||||
sendDmpWorker({
|
||||
msg: 'create patch',
|
||||
lastDoc: lastContent,
|
||||
currDoc: content
|
||||
}, function (err, patch) {
|
||||
if (err) logger.error('save note revision error', err)
|
||||
if (!patch) {
|
||||
// if patch is empty (means no difference) then just update the latest revision updated time
|
||||
latestRevision.changed('updatedAt', true)
|
||||
latestRevision.update({
|
||||
updatedAt: Date.now()
|
||||
}).then(function (revision) {
|
||||
Revision.finishSaveNoteRevision(note, revision, callback)
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
} else {
|
||||
var latestRevision = revisions[0]
|
||||
var lastContent = latestRevision.content || latestRevision.lastContent
|
||||
var content = note.content
|
||||
sendDmpWorker({
|
||||
msg: 'create patch',
|
||||
lastDoc: lastContent,
|
||||
currDoc: content
|
||||
}, function (err, patch) {
|
||||
if (err) logger.error('save note revision error', err)
|
||||
if (!patch) {
|
||||
// if patch is empty (means no difference) then just update the latest revision updated time
|
||||
latestRevision.changed('updatedAt', true)
|
||||
latestRevision.update({
|
||||
updatedAt: Date.now()
|
||||
}).then(function (revision) {
|
||||
Revision.finishSaveNoteRevision(note, revision, callback)
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
} else {
|
||||
Revision.create({
|
||||
noteId: note.id,
|
||||
patch: patch,
|
||||
content: note.content,
|
||||
length: note.content.length,
|
||||
authorship: note.authorship
|
||||
}).then(function (revision) {
|
||||
// clear last revision content to reduce db size
|
||||
latestRevision.update({
|
||||
content: null
|
||||
}).then(function () {
|
||||
Revision.finishSaveNoteRevision(note, revision, callback)
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
Revision.create({
|
||||
noteId: note.id,
|
||||
patch: patch,
|
||||
content: note.content,
|
||||
length: note.content.length,
|
||||
authorship: note.authorship
|
||||
}).then(function (revision) {
|
||||
// clear last revision content to reduce db size
|
||||
latestRevision.update({
|
||||
content: null
|
||||
}).then(function () {
|
||||
Revision.finishSaveNoteRevision(note, revision, callback)
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
},
|
||||
finishSaveNoteRevision: function (note, revision, callback) {
|
||||
note.update({
|
||||
savedAt: revision.updatedAt
|
||||
}).then(function () {
|
||||
return callback(null, revision)
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
Revision.finishSaveNoteRevision = function (note, revision, callback) {
|
||||
note.update({
|
||||
savedAt: revision.updatedAt
|
||||
}).then(function () {
|
||||
return callback(null, revision)
|
||||
}).catch(function (err) {
|
||||
return callback(err, null)
|
||||
})
|
||||
}
|
||||
|
||||
return Revision
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
'use strict'
|
||||
// external modules
|
||||
var Sequelize = require('sequelize')
|
||||
var scrypt = require('@mlink/scrypt')
|
||||
var scrypt = require('scrypt')
|
||||
|
||||
// core
|
||||
var logger = require('../logger')
|
||||
var {generateAvatarURL} = require('../letter-avatars')
|
||||
var { generateAvatarURL } = require('../letter-avatars')
|
||||
|
||||
module.exports = function (sequelize, DataTypes) {
|
||||
var User = sequelize.define('User', {
|
||||
|
@ -47,111 +47,108 @@ module.exports = function (sequelize, DataTypes) {
|
|||
this.setDataValue('password', hash)
|
||||
}
|
||||
}
|
||||
}, {
|
||||
instanceMethods: {
|
||||
verifyPassword: function (attempt) {
|
||||
if (scrypt.verifyKdfSync(Buffer.from(this.password, 'hex'), attempt)) {
|
||||
return this
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
classMethods: {
|
||||
associate: function (models) {
|
||||
User.hasMany(models.Note, {
|
||||
foreignKey: 'ownerId',
|
||||
constraints: false
|
||||
})
|
||||
User.hasMany(models.Note, {
|
||||
foreignKey: 'lastchangeuserId',
|
||||
constraints: false
|
||||
})
|
||||
},
|
||||
getProfile: function (user) {
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null)
|
||||
},
|
||||
parseProfile: function (profile) {
|
||||
try {
|
||||
profile = JSON.parse(profile)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
profile = null
|
||||
}
|
||||
if (profile) {
|
||||
profile = {
|
||||
name: profile.displayName || profile.username,
|
||||
photo: User.parsePhotoByProfile(profile),
|
||||
biggerphoto: User.parsePhotoByProfile(profile, true)
|
||||
}
|
||||
}
|
||||
return profile
|
||||
},
|
||||
parsePhotoByProfile: function (profile, bigger) {
|
||||
var photo = null
|
||||
switch (profile.provider) {
|
||||
case 'facebook':
|
||||
photo = 'https://graph.facebook.com/' + profile.id + '/picture'
|
||||
if (bigger) photo += '?width=400'
|
||||
else photo += '?width=96'
|
||||
break
|
||||
case 'twitter':
|
||||
photo = 'https://twitter.com/' + profile.username + '/profile_image'
|
||||
if (bigger) photo += '?size=original'
|
||||
else photo += '?size=bigger'
|
||||
break
|
||||
case 'github':
|
||||
photo = 'https://avatars.githubusercontent.com/u/' + profile.id
|
||||
if (bigger) photo += '?s=400'
|
||||
else photo += '?s=96'
|
||||
break
|
||||
case 'gitlab':
|
||||
photo = profile.avatarUrl
|
||||
if (photo) {
|
||||
if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400')
|
||||
else photo = photo.replace(/(\?s=)\d*$/i, '$196')
|
||||
} else {
|
||||
photo = generateAvatarURL(profile.username)
|
||||
}
|
||||
break
|
||||
case 'mattermost':
|
||||
photo = profile.avatarUrl
|
||||
if (photo) {
|
||||
if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400')
|
||||
else photo = photo.replace(/(\?s=)\d*$/i, '$196')
|
||||
} else {
|
||||
photo = generateAvatarURL(profile.username)
|
||||
}
|
||||
break
|
||||
case 'dropbox':
|
||||
photo = generateAvatarURL('', profile.emails[0].value, bigger)
|
||||
break
|
||||
case 'google':
|
||||
photo = profile.photos[0].value
|
||||
if (bigger) photo = photo.replace(/(\?sz=)\d*$/i, '$1400')
|
||||
else photo = photo.replace(/(\?sz=)\d*$/i, '$196')
|
||||
break
|
||||
case 'ldap':
|
||||
photo = generateAvatarURL(profile.username, profile.emails[0], bigger)
|
||||
break
|
||||
case 'saml':
|
||||
photo = generateAvatarURL(profile.username, profile.emails[0], bigger)
|
||||
break
|
||||
}
|
||||
return photo
|
||||
},
|
||||
parseProfileByEmail: function (email) {
|
||||
return {
|
||||
name: email.substring(0, email.lastIndexOf('@')),
|
||||
photo: generateAvatarURL('', email, false),
|
||||
biggerphoto: generateAvatarURL('', email, true)
|
||||
}
|
||||
})
|
||||
|
||||
User.prototype.verifyPassword = function (attempt) {
|
||||
if (scrypt.verifyKdfSync(Buffer.from(this.password, 'hex'), attempt)) {
|
||||
return this
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
User.associate = function (models) {
|
||||
User.hasMany(models.Note, {
|
||||
foreignKey: 'ownerId',
|
||||
constraints: false
|
||||
})
|
||||
User.hasMany(models.Note, {
|
||||
foreignKey: 'lastchangeuserId',
|
||||
constraints: false
|
||||
})
|
||||
}
|
||||
User.getProfile = function (user) {
|
||||
if (!user) {
|
||||
return null
|
||||
}
|
||||
return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null)
|
||||
}
|
||||
User.parseProfile = function (profile) {
|
||||
try {
|
||||
profile = JSON.parse(profile)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
profile = null
|
||||
}
|
||||
if (profile) {
|
||||
profile = {
|
||||
name: profile.displayName || profile.username,
|
||||
photo: User.parsePhotoByProfile(profile),
|
||||
biggerphoto: User.parsePhotoByProfile(profile, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
return profile
|
||||
}
|
||||
User.parsePhotoByProfile = function (profile, bigger) {
|
||||
var photo = null
|
||||
switch (profile.provider) {
|
||||
case 'facebook':
|
||||
photo = 'https://graph.facebook.com/' + profile.id + '/picture'
|
||||
if (bigger) photo += '?width=400'
|
||||
else photo += '?width=96'
|
||||
break
|
||||
case 'twitter':
|
||||
photo = 'https://twitter.com/' + profile.username + '/profile_image'
|
||||
if (bigger) photo += '?size=original'
|
||||
else photo += '?size=bigger'
|
||||
break
|
||||
case 'github':
|
||||
photo = 'https://avatars.githubusercontent.com/u/' + profile.id
|
||||
if (bigger) photo += '?s=400'
|
||||
else photo += '?s=96'
|
||||
break
|
||||
case 'gitlab':
|
||||
photo = profile.avatarUrl
|
||||
if (photo) {
|
||||
if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400')
|
||||
else photo = photo.replace(/(\?s=)\d*$/i, '$196')
|
||||
} else {
|
||||
photo = generateAvatarURL(profile.username)
|
||||
}
|
||||
break
|
||||
case 'mattermost':
|
||||
photo = profile.avatarUrl
|
||||
if (photo) {
|
||||
if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400')
|
||||
else photo = photo.replace(/(\?s=)\d*$/i, '$196')
|
||||
} else {
|
||||
photo = generateAvatarURL(profile.username)
|
||||
}
|
||||
break
|
||||
case 'dropbox':
|
||||
photo = generateAvatarURL('', profile.emails[0].value, bigger)
|
||||
break
|
||||
case 'google':
|
||||
photo = profile.photos[0].value
|
||||
if (bigger) photo = photo.replace(/(\?sz=)\d*$/i, '$1400')
|
||||
else photo = photo.replace(/(\?sz=)\d*$/i, '$196')
|
||||
break
|
||||
case 'ldap':
|
||||
photo = generateAvatarURL(profile.username, profile.emails[0], bigger)
|
||||
break
|
||||
case 'saml':
|
||||
photo = generateAvatarURL(profile.username, profile.emails[0], bigger)
|
||||
break
|
||||
}
|
||||
return photo
|
||||
}
|
||||
User.parseProfileByEmail = function (email) {
|
||||
return {
|
||||
name: email.substring(0, email.lastIndexOf('@')),
|
||||
photo: generateAvatarURL('', email, false),
|
||||
biggerphoto: generateAvatarURL('', email, true)
|
||||
}
|
||||
}
|
||||
|
||||
return User
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ var utils = require('./utils')
|
|||
// public
|
||||
var response = {
|
||||
errorForbidden: function (res) {
|
||||
const {req} = res
|
||||
const { req } = res
|
||||
if (req.user) {
|
||||
responseError(res, '403', 'Forbidden', 'oh no.')
|
||||
} else {
|
||||
|
@ -549,16 +549,16 @@ function gitlabActionProjects (req, res, note) {
|
|||
ret.accesstoken = user.accessToken
|
||||
ret.profileid = user.profileid
|
||||
request(
|
||||
config.gitlab.baseURL + '/api/' + config.gitlab.version + '/projects?membership=yes&per_page=100&access_token=' + user.accessToken,
|
||||
function (error, httpResponse, body) {
|
||||
if (!error && httpResponse.statusCode === 200) {
|
||||
ret.projects = JSON.parse(body)
|
||||
return res.send(ret)
|
||||
} else {
|
||||
return res.send(ret)
|
||||
}
|
||||
}
|
||||
)
|
||||
config.gitlab.baseURL + '/api/' + config.gitlab.version + '/projects?membership=yes&per_page=100&access_token=' + user.accessToken,
|
||||
function (error, httpResponse, body) {
|
||||
if (!error && httpResponse.statusCode === 200) {
|
||||
ret.projects = JSON.parse(body)
|
||||
return res.send(ret)
|
||||
} else {
|
||||
return res.send(ret)
|
||||
}
|
||||
}
|
||||
)
|
||||
}).catch(function (err) {
|
||||
logger.error('gitlab action projects failed: ' + err)
|
||||
return response.errorInternalError(res)
|
||||
|
|
|
@ -4,7 +4,7 @@ const Router = require('express').Router
|
|||
const passport = require('passport')
|
||||
const DropboxStrategy = require('passport-dropbox-oauth2').Strategy
|
||||
const config = require('../../../config')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let dropboxAuth = module.exports = Router()
|
||||
|
||||
|
|
|
@ -7,8 +7,8 @@ const LocalStrategy = require('passport-local').Strategy
|
|||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const {setReturnToFromReferer} = require('../utils')
|
||||
const {urlencodedParser} = require('../../utils')
|
||||
const { setReturnToFromReferer } = require('../utils')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const response = require('../../../response')
|
||||
|
||||
let emailAuth = module.exports = Router()
|
||||
|
|
|
@ -5,7 +5,7 @@ const passport = require('passport')
|
|||
const FacebookStrategy = require('passport-facebook').Strategy
|
||||
|
||||
const config = require('../../../config')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let facebookAuth = module.exports = Router()
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ const passport = require('passport')
|
|||
const GithubStrategy = require('passport-github').Strategy
|
||||
const config = require('../../../config')
|
||||
const response = require('../../../response')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let githubAuth = module.exports = Router()
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ const passport = require('passport')
|
|||
const GitlabStrategy = require('passport-gitlab2').Strategy
|
||||
const config = require('../../../config')
|
||||
const response = require('../../../response')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let gitlabAuth = module.exports = Router()
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ const Router = require('express').Router
|
|||
const passport = require('passport')
|
||||
var GoogleStrategy = require('passport-google-oauth20').Strategy
|
||||
const config = require('../../../config')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let googleAuth = module.exports = Router()
|
||||
|
||||
|
@ -12,14 +12,14 @@ passport.use(new GoogleStrategy({
|
|||
clientID: config.google.clientID,
|
||||
clientSecret: config.google.clientSecret,
|
||||
callbackURL: config.serverURL + '/auth/google/callback',
|
||||
userProfileURL: "https://www.googleapis.com/oauth2/v3/userinfo"
|
||||
userProfileURL: 'https://www.googleapis.com/oauth2/v3/userinfo'
|
||||
}, passportGeneralCallback))
|
||||
|
||||
googleAuth.get('/auth/google', function (req, res, next) {
|
||||
setReturnToFromReferer(req)
|
||||
passport.authenticate('google', { scope: ['profile'] })(req, res, next)
|
||||
})
|
||||
// google auth callback
|
||||
// google auth callback
|
||||
googleAuth.get('/auth/google/callback',
|
||||
passport.authenticate('google', {
|
||||
successReturnToOrRedirect: config.serverURL + '/',
|
||||
|
|
|
@ -6,8 +6,8 @@ const LDAPStrategy = require('passport-ldapauth')
|
|||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const {setReturnToFromReferer} = require('../utils')
|
||||
const {urlencodedParser} = require('../../utils')
|
||||
const { setReturnToFromReferer } = require('../utils')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const response = require('../../../response')
|
||||
|
||||
let ldapAuth = module.exports = Router()
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
'use strict'
|
||||
|
||||
require('babel-polyfill')
|
||||
require('isomorphic-fetch');
|
||||
const Router = require('express').Router
|
||||
const passport = require('passport')
|
||||
const Mattermost = require('mattermost')
|
||||
const MattermostClient = require('mattermost-redux/client/client4').default
|
||||
const OAuthStrategy = require('passport-oauth2').Strategy
|
||||
const config = require('../../../config')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
|
||||
const mattermost = new Mattermost.Client()
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let mattermostAuth = module.exports = Router()
|
||||
|
||||
const mattermostClient = new MattermostClient()
|
||||
|
||||
let mattermostStrategy = new OAuthStrategy({
|
||||
authorizationURL: config.mattermost.baseURL + '/oauth/authorize',
|
||||
tokenURL: config.mattermost.baseURL + '/oauth/access_token',
|
||||
|
@ -20,17 +21,11 @@ let mattermostStrategy = new OAuthStrategy({
|
|||
}, passportGeneralCallback)
|
||||
|
||||
mattermostStrategy.userProfile = (accessToken, done) => {
|
||||
mattermost.setUrl(config.mattermost.baseURL)
|
||||
mattermost.token = accessToken
|
||||
mattermost.useHeaderToken()
|
||||
mattermost.getMe(
|
||||
(data) => {
|
||||
done(null, data)
|
||||
},
|
||||
(err) => {
|
||||
done(err)
|
||||
}
|
||||
)
|
||||
mattermostClient.setUrl(config.mattermost.baseURL)
|
||||
mattermostClient.setToken(accessToken)
|
||||
mattermostClient.getMe()
|
||||
.then((data) => done(null, data))
|
||||
.catch((err) => done(err))
|
||||
}
|
||||
|
||||
passport.use(mattermostStrategy)
|
||||
|
|
|
@ -4,7 +4,7 @@ const Router = require('express').Router
|
|||
const passport = require('passport')
|
||||
const { Strategy, InternalOAuthError } = require('passport-oauth2')
|
||||
const config = require('../../../config')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let oauth2Auth = module.exports = Router()
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@ const OpenIDStrategy = require('@passport-next/passport-openid').Strategy
|
|||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const {urlencodedParser} = require('../../utils')
|
||||
const {setReturnToFromReferer} = require('../utils')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const { setReturnToFromReferer } = require('../utils')
|
||||
|
||||
let openIDAuth = module.exports = Router()
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ const SamlStrategy = require('passport-saml').Strategy
|
|||
const config = require('../../../config')
|
||||
const models = require('../../../models')
|
||||
const logger = require('../../../logger')
|
||||
const {urlencodedParser} = require('../../utils')
|
||||
const { urlencodedParser } = require('../../utils')
|
||||
const fs = require('fs')
|
||||
const intersection = function (array1, array2) { return array1.filter((n) => array2.includes(n)) }
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ const passport = require('passport')
|
|||
const TwitterStrategy = require('passport-twitter').Strategy
|
||||
|
||||
const config = require('../../../config')
|
||||
const {setReturnToFromReferer, passportGeneralCallback} = require('../utils')
|
||||
const { setReturnToFromReferer, passportGeneralCallback } = require('../utils')
|
||||
|
||||
let twitterAuth = module.exports = Router()
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
const Router = require('express').Router
|
||||
|
||||
const {urlencodedParser} = require('./utils')
|
||||
const { urlencodedParser } = require('./utils')
|
||||
const history = require('../history')
|
||||
const historyRouter = module.exports = Router()
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
const config = require('../../config')
|
||||
const logger = require('../../logger')
|
||||
|
||||
const imgur = require('imgur')
|
||||
const imgur = require('@hackmd/imgur')
|
||||
|
||||
exports.uploadImage = function (imagePath, callback) {
|
||||
if (!imagePath || typeof imagePath !== 'string') {
|
||||
|
@ -17,12 +17,12 @@ exports.uploadImage = function (imagePath, callback) {
|
|||
|
||||
imgur.setClientId(config.imgur.clientID)
|
||||
imgur.uploadFile(imagePath)
|
||||
.then(function (json) {
|
||||
if (config.debug) {
|
||||
logger.info('SERVER uploadimage success: ' + JSON.stringify(json))
|
||||
}
|
||||
callback(null, json.data.link.replace(/^http:\/\//i, 'https://'))
|
||||
}).catch(function (err) {
|
||||
callback(new Error(err), null)
|
||||
})
|
||||
.then(function (json) {
|
||||
if (config.debug) {
|
||||
logger.info('SERVER uploadimage success: ' + JSON.stringify(json))
|
||||
}
|
||||
callback(null, json.data.link.replace(/^http:\/\//i, 'https://'))
|
||||
}).catch(function (err) {
|
||||
callback(new Error(err), null)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ const fs = require('fs')
|
|||
const path = require('path')
|
||||
|
||||
const config = require('../../config')
|
||||
const {getImageMimeType} = require('../../utils')
|
||||
const { getImageMimeType } = require('../../utils')
|
||||
const logger = require('../../logger')
|
||||
|
||||
const Minio = require('minio')
|
||||
|
|
|
@ -3,7 +3,7 @@ const fs = require('fs')
|
|||
const path = require('path')
|
||||
|
||||
const config = require('../../config')
|
||||
const {getImageMimeType} = require('../../utils')
|
||||
const { getImageMimeType } = require('../../utils')
|
||||
const logger = require('../../logger')
|
||||
|
||||
const AWS = require('aws-sdk')
|
||||
|
|
|
@ -4,7 +4,7 @@ const Router = require('express').Router
|
|||
|
||||
const response = require('../response')
|
||||
|
||||
const {markdownParser} = require('./utils')
|
||||
const { markdownParser } = require('./utils')
|
||||
|
||||
const noteRouter = module.exports = Router()
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ const config = require('../config')
|
|||
const models = require('../models')
|
||||
const logger = require('../logger')
|
||||
|
||||
const {urlencodedParser} = require('./utils')
|
||||
const { urlencodedParser } = require('./utils')
|
||||
|
||||
const statusRouter = module.exports = Router()
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ const response = require('../response')
|
|||
const config = require('../config')
|
||||
const models = require('../models')
|
||||
const logger = require('../logger')
|
||||
const {generateAvatar} = require('../letter-avatars')
|
||||
const { generateAvatar } = require('../letter-avatars')
|
||||
|
||||
const UserRouter = module.exports = Router()
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict'
|
||||
// external modules
|
||||
var DiffMatchPatch = require('diff-match-patch')
|
||||
var DiffMatchPatch = require('@hackmd/diff-match-patch')
|
||||
var dmp = new DiffMatchPatch()
|
||||
|
||||
// core
|
||||
|
|
374
package.json
374
package.json
|
@ -1,152 +1,194 @@
|
|||
{
|
||||
"name": "CodiMD",
|
||||
"name": "codimd",
|
||||
"version": "1.3.1",
|
||||
"description": "Realtime collaborative markdown notes on all platforms.",
|
||||
"main": "app.js",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"test": "npm run-script eslint && npm run-script jsonlint && mocha",
|
||||
"eslint": "node_modules/.bin/eslint lib public test app.js",
|
||||
"jsonlint": "find . -not -path './node_modules/*' -type f -name '*.json' -o -type f -name '*.json.example' | while read json; do echo $json ; jq . $json; done",
|
||||
"standard": "echo 'standard is no longer being used, use `npm run eslint` instead!' && exit 1",
|
||||
"dev": "webpack --config webpack.dev.js --progress --colors --watch",
|
||||
"build": "webpack --config webpack.prod.js --progress --colors --bail",
|
||||
"postinstall": "bin/heroku",
|
||||
"start": "sequelize db:migrate && node app.js",
|
||||
"doctoc": "doctoc --title='# Table of Contents' README.md"
|
||||
},
|
||||
"dependencies": {
|
||||
"@passport-next/passport-openid": "^1.0.0",
|
||||
"Idle.Js": "git+https://github.com/shawnmclean/Idle.js",
|
||||
"archiver": "^2.1.1",
|
||||
"async": "^2.1.4",
|
||||
"aws-sdk": "^2.345.0",
|
||||
"azure-storage": "^2.7.0",
|
||||
"base64url": "^3.0.0",
|
||||
"body-parser": "^1.15.2",
|
||||
"bootstrap": "^3.4.0",
|
||||
"bootstrap-validator": "^0.11.8",
|
||||
"chance": "^1.0.4",
|
||||
"cheerio": "^0.22.0",
|
||||
"codemirror": "git+https://github.com/hackmdio/CodeMirror.git",
|
||||
"compression": "^1.6.2",
|
||||
"connect-flash": "^0.1.1",
|
||||
"connect-session-sequelize": "^4.1.0",
|
||||
"cookie": "0.3.1",
|
||||
"cookie-parser": "1.4.3",
|
||||
"deep-freeze": "^0.0.1",
|
||||
"diff-match-patch": "git+https://github.com/hackmdio/diff-match-patch.git",
|
||||
"ejs": "^2.5.5",
|
||||
"emojify.js": "~1.1.0",
|
||||
"express": ">=4.14",
|
||||
"express-session": "^1.14.2",
|
||||
"file-saver": "^1.3.3",
|
||||
"flowchart.js": "^1.6.4",
|
||||
"fork-awesome": "^1.1.3",
|
||||
"formidable": "^1.0.17",
|
||||
"gist-embed": "~2.6.0",
|
||||
"graceful-fs": "^4.1.11",
|
||||
"handlebars": "^4.0.13",
|
||||
"helmet": "^3.13.0",
|
||||
"highlight.js": "~9.12.0",
|
||||
"i18n": "^0.8.3",
|
||||
"imgur": "git+https://github.com/hackmdio/node-imgur.git",
|
||||
"ionicons": "~2.0.1",
|
||||
"jquery": "^3.1.1",
|
||||
"jquery-mousewheel": "^3.1.13",
|
||||
"jquery-ui": "^1.12.1",
|
||||
"js-cookie": "^2.1.3",
|
||||
"js-sequence-diagrams": "^1000000.0.6",
|
||||
"js-url": "^2.3.0",
|
||||
"js-yaml": "^3.7.0",
|
||||
"jsdom-nogyp": "^0.8.3",
|
||||
"keymaster": "^1.6.2",
|
||||
"list.js": "^1.5.0",
|
||||
"lodash": "^4.17.11",
|
||||
"lz-string": "git+https://github.com/hackmdio/lz-string.git",
|
||||
"markdown-it": "^8.2.2",
|
||||
"markdown-it-abbr": "^1.0.4",
|
||||
"markdown-it-container": "^2.0.0",
|
||||
"markdown-it-deflist": "^2.0.1",
|
||||
"markdown-it-emoji": "^1.3.0",
|
||||
"markdown-it-footnote": "^3.0.1",
|
||||
"markdown-it-imsize": "^2.0.1",
|
||||
"markdown-it-ins": "^2.0.0",
|
||||
"markdown-it-mark": "^2.0.0",
|
||||
"markdown-it-mathjax": "^2.0.0",
|
||||
"markdown-it-regexp": "^0.4.0",
|
||||
"markdown-it-sub": "^1.0.0",
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"markdown-pdf": "^9.0.0",
|
||||
"mathjax": "~2.7.0",
|
||||
"mattermost": "^3.4.0",
|
||||
"mermaid": "~7.1.0",
|
||||
"meta-marked": "^0.4.2",
|
||||
"method-override": "^2.3.7",
|
||||
"minimist": "^1.2.0",
|
||||
"minio": "^6.0.0",
|
||||
"moment": "^2.17.1",
|
||||
"morgan": "^1.7.0",
|
||||
"mysql": "^2.12.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-dropbox-oauth2": "^1.1.0",
|
||||
"passport-facebook": "^2.1.1",
|
||||
"passport-github": "^1.1.0",
|
||||
"passport-gitlab2": "^4.0.0",
|
||||
"passport-google-oauth20": "^1.0.0",
|
||||
"passport-ldapauth": "^2.0.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-oauth2": "^1.4.0",
|
||||
"passport-saml": "^1.0.0",
|
||||
"passport-twitter": "^1.0.4",
|
||||
"passport.socketio": "^3.7.0",
|
||||
"pdfobject": "^2.0.201604172",
|
||||
"pg": "^6.1.2",
|
||||
"pg-hstore": "^2.3.2",
|
||||
"prismjs": "^1.6.0",
|
||||
"randomcolor": "^0.5.3",
|
||||
"raphael": "git+https://github.com/dmitrybaranovskiy/raphael",
|
||||
"readline-sync": "^1.4.7",
|
||||
"request": "^2.88.0",
|
||||
"reveal.js": "~3.7.0",
|
||||
"@mlink/scrypt": "^6.1.2",
|
||||
"select2": "^3.5.2-browserify",
|
||||
"sequelize": "^3.28.0",
|
||||
"sequelize-cli": "^2.5.1",
|
||||
"shortid": "2.2.8",
|
||||
"socket.io": "~2.1.1",
|
||||
"socket.io-client": "~2.1.1",
|
||||
"spin.js": "^2.3.2",
|
||||
"sqlite3": "^4.0.1",
|
||||
"store": "^2.0.12",
|
||||
"string": "^3.3.3",
|
||||
"tedious": "^1.14.0",
|
||||
"toobusy-js": "^0.5.1",
|
||||
"turndown": "^5.0.1",
|
||||
"uuid": "^3.1.0",
|
||||
"validator": "^10.4.0",
|
||||
"velocity-animate": "^1.4.0",
|
||||
"visibilityjs": "^1.2.4",
|
||||
"viz.js": "^1.7.0",
|
||||
"winston": "^3.1.0",
|
||||
"ws": "^6.0.0",
|
||||
"xss": "^1.0.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/tough-cookie": "~2.4.0",
|
||||
"**/minimatch": "^3.0.2",
|
||||
"**/request": "^2.88.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.x"
|
||||
},
|
||||
"bugs": "https://github.com/hackmdio/codimd/issues",
|
||||
"keywords": [
|
||||
"Collaborative",
|
||||
"Markdown",
|
||||
"Notes"
|
||||
],
|
||||
"homepage": "https://codimd.org",
|
||||
"bugs": "https://github.com/hackmdio/codimd/issues",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/hackmdio/codimd.git"
|
||||
},
|
||||
"license": "AGPL-3.0",
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"build": "webpack --config webpack.prod.js --progress --colors --bail",
|
||||
"dev": "webpack --config webpack.dev.js --progress --colors --watch",
|
||||
"doctoc": "doctoc --title='# Table of Contents' README.md",
|
||||
"eslint": "eslint lib public test app.js",
|
||||
"postinstall": "bin/heroku",
|
||||
"jsonlint": "find . -not -path './node_modules/*' -type f -name '*.json' -o -type f -name '*.json.example' | while read json; do echo $json ; jq . $json; done",
|
||||
"standard": "echo 'standard is no longer being used, use `npm run eslint` instead!' && exit 1",
|
||||
"start": "sequelize db:migrate && node app.js",
|
||||
"test": "npm run-script eslint && npm run-script jsonlint && mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hackmd/codemirror": "~5.41.2",
|
||||
"@hackmd/diff-match-patch": "~1.1.1",
|
||||
"@hackmd/idle-js": "~1.0.1",
|
||||
"@hackmd/imgur": "~0.4.1",
|
||||
"@hackmd/js-sequence-diagrams": "~0.0.1-alpha.3",
|
||||
"@hackmd/lz-string": "~1.4.4",
|
||||
"@hackmd/meta-marked": "~0.4.4",
|
||||
"@passport-next/passport-openid": "~1.0.0",
|
||||
"archiver": "~2.1.1",
|
||||
"async": "~2.1.4",
|
||||
"aws-sdk": "~2.345.0",
|
||||
"azure-storage": "~2.10.2",
|
||||
"base64url": "~3.0.0",
|
||||
"body-parser": "~1.18.3",
|
||||
"bootstrap": "~3.4.0",
|
||||
"bootstrap-validator": "~0.11.8",
|
||||
"chance": "~1.0.4",
|
||||
"cheerio": "~0.22.0",
|
||||
"compression": "~1.7.4",
|
||||
"connect-flash": "~0.1.1",
|
||||
"connect-session-sequelize": "~6.0.0",
|
||||
"cookie": "~0.3.1",
|
||||
"cookie-parser": "~1.4.3",
|
||||
"deep-freeze": "~0.0.1",
|
||||
"ejs": "~2.5.5",
|
||||
"emojify.js": "~1.1.0",
|
||||
"express": "~4.16.4",
|
||||
"express-session": "~1.16.1",
|
||||
"file-saver": "~1.3.3",
|
||||
"flowchart.js": "~1.6.4",
|
||||
"fork-awesome": "~1.1.3",
|
||||
"formidable": "~1.2.1",
|
||||
"gist-embed": "~2.6.0",
|
||||
"graceful-fs": "~4.1.11",
|
||||
"handlebars": "~4.0.13",
|
||||
"helmet": "~3.13.0",
|
||||
"highlight.js": "~9.12.0",
|
||||
"i18n": "~0.8.3",
|
||||
"ionicons": "~2.0.1",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"jquery": "~3.1.1",
|
||||
"jquery-mousewheel": "~3.1.13",
|
||||
"jquery-ui": "~1.12.1",
|
||||
"js-cookie": "~2.1.3",
|
||||
"js-yaml": "~3.13.1",
|
||||
"jsdom-nogyp": "~0.8.3",
|
||||
"keymaster": "~1.6.2",
|
||||
"list.js": "~1.5.0",
|
||||
"lodash": "~4.17.11",
|
||||
"markdown-it": "~8.2.2",
|
||||
"markdown-it-abbr": "~1.0.4",
|
||||
"markdown-it-container": "~2.0.0",
|
||||
"markdown-it-deflist": "~2.0.1",
|
||||
"markdown-it-emoji": "~1.3.0",
|
||||
"markdown-it-footnote": "~3.0.1",
|
||||
"markdown-it-imsize": "~2.0.1",
|
||||
"markdown-it-ins": "~2.0.0",
|
||||
"markdown-it-mark": "~2.0.0",
|
||||
"markdown-it-mathjax": "~2.0.0",
|
||||
"markdown-it-regexp": "~0.4.0",
|
||||
"markdown-it-sub": "~1.0.0",
|
||||
"markdown-it-sup": "~1.0.0",
|
||||
"markdown-pdf": "~9.0.0",
|
||||
"mathjax": "~2.7.0",
|
||||
"mattermost-redux": "^5.9.0",
|
||||
"mermaid": "~7.1.0",
|
||||
"method-override": "~2.3.7",
|
||||
"minimist": "~1.2.0",
|
||||
"minio": "~6.0.0",
|
||||
"moment": "~2.24.0",
|
||||
"morgan": "~1.9.1",
|
||||
"mysql": "~2.16.0",
|
||||
"passport": "~0.4.0",
|
||||
"passport-dropbox-oauth2": "~1.1.0",
|
||||
"passport-facebook": "~2.1.1",
|
||||
"passport-github": "~1.1.0",
|
||||
"passport-gitlab2": "~4.0.0",
|
||||
"passport-google-oauth20": "~1.0.0",
|
||||
"passport-ldapauth": "~2.1.3",
|
||||
"passport-local": "~1.0.0",
|
||||
"passport-oauth2": "~1.4.0",
|
||||
"passport-saml": "~1.0.0",
|
||||
"passport-twitter": "~1.0.4",
|
||||
"passport.socketio": "~3.7.0",
|
||||
"pdfobject": "~2.0.201604172",
|
||||
"pg": "~6.1.2",
|
||||
"pg-hstore": "~2.3.2",
|
||||
"prismjs": "~1.6.0",
|
||||
"randomcolor": "~0.5.3",
|
||||
"raphael": "~2.2.8",
|
||||
"readline-sync": "~1.4.7",
|
||||
"request": "~2.88.0",
|
||||
"reveal.js": "~3.7.0",
|
||||
"scrypt": "~6.0.3",
|
||||
"select2": "~3.5.2-browserify",
|
||||
"sequelize": "5.3.5",
|
||||
"shortid": "~2.2.8",
|
||||
"socket.io": "~2.1.1",
|
||||
"socket.io-client": "~2.1.1",
|
||||
"spin.js": "~2.3.2",
|
||||
"sqlite3": "~4.0.1",
|
||||
"store": "~2.0.12",
|
||||
"tedious": "~6.1.0",
|
||||
"toobusy-js": "~0.5.1",
|
||||
"turndown": "~5.0.1",
|
||||
"uuid": "~3.1.0",
|
||||
"validator": "~10.4.0",
|
||||
"velocity-animate": "~1.4.0",
|
||||
"visibilityjs": "~1.2.4",
|
||||
"viz.js": "~1.7.0",
|
||||
"winston": "~3.1.0",
|
||||
"ws": "~6.0.0",
|
||||
"wurl": "~2.5.3",
|
||||
"xss": "~1.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"acorn": "~6.1.1",
|
||||
"babel-core": "~6.26.3",
|
||||
"babel-loader": "~7.1.4",
|
||||
"babel-plugin-transform-runtime": "~6.23.0",
|
||||
"babel-polyfill": "~6.26.0",
|
||||
"babel-preset-env": "~1.7.0",
|
||||
"babel-runtime": "~6.26.0",
|
||||
"copy-webpack-plugin": "~4.5.2",
|
||||
"css-loader": "~1.0.0",
|
||||
"doctoc": "~1.4.0",
|
||||
"ejs-loader": "~0.3.1",
|
||||
"eslint": "~5.16.0",
|
||||
"eslint-config-standard": "~12.0.0",
|
||||
"eslint-plugin-import": "~2.17.1",
|
||||
"eslint-plugin-node": "~8.0.1",
|
||||
"eslint-plugin-promise": "~4.1.1",
|
||||
"eslint-plugin-standard": "~4.0.0",
|
||||
"exports-loader": "~0.7.0",
|
||||
"expose-loader": "~0.7.5",
|
||||
"file-loader": "~2.0.0",
|
||||
"html-webpack-plugin": "~4.0.0-beta.2",
|
||||
"imports-loader": "~0.8.0",
|
||||
"jsonlint": "~1.6.2",
|
||||
"less": "~3.9.0",
|
||||
"less-loader": "~4.1.0",
|
||||
"mini-css-extract-plugin": "~0.4.1",
|
||||
"mocha": "~5.2.0",
|
||||
"mock-require": "~3.0.3",
|
||||
"optimize-css-assets-webpack-plugin": "~5.0.0",
|
||||
"script-loader": "~0.7.2",
|
||||
"sequelize-cli": "~5.4.0",
|
||||
"string-loader": "~0.0.1",
|
||||
"style-loader": "~0.21.0",
|
||||
"uglifyjs-webpack-plugin": "~1.2.7",
|
||||
"url-loader": "~1.0.1",
|
||||
"webpack": "~4.30.0",
|
||||
"webpack-cli": "~3.3.0",
|
||||
"webpack-merge": "~4.1.4",
|
||||
"webpack-parallel-uglify-plugin": "~1.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "~4.0.0",
|
||||
"utf-8-validate": "~5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"maintainers": [
|
||||
{
|
||||
"name": "Max Wu",
|
||||
|
@ -156,53 +198,5 @@
|
|||
"name": "Christoph (Sheogorath) Kern",
|
||||
"email": "codimd@sheogorath.shivering-isles.com"
|
||||
}
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/hackmdio/codimd.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"copy-webpack-plugin": "^4.5.2",
|
||||
"css-loader": "^1.0.0",
|
||||
"doctoc": "^1.4.0",
|
||||
"ejs-loader": "^0.3.1",
|
||||
"eslint": "^5.9.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-node": "^8.0.0",
|
||||
"eslint-plugin-promise": "^4.0.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"exports-loader": "^0.7.0",
|
||||
"expose-loader": "^0.7.5",
|
||||
"file-loader": "^2.0.0",
|
||||
"html-webpack-plugin": "4.0.0-beta.2",
|
||||
"imports-loader": "^0.8.0",
|
||||
"jsonlint": "^1.6.2",
|
||||
"less": "^2.7.1",
|
||||
"less-loader": "^4.1.0",
|
||||
"mini-css-extract-plugin": "^0.4.1",
|
||||
"mocha": "^5.2.0",
|
||||
"mock-require": "^3.0.3",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.0",
|
||||
"script-loader": "^0.7.2",
|
||||
"string-loader": "^0.0.1",
|
||||
"style-loader": "^0.21.0",
|
||||
"uglifyjs-webpack-plugin": "^1.2.7",
|
||||
"url-loader": "^1.0.1",
|
||||
"webpack": "^4.14.0",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"webpack-merge": "^4.1.4",
|
||||
"webpack-parallel-uglify-plugin": "^1.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.0",
|
||||
"utf-8-validate": "^5.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ module.exports = {
|
|||
"ui": false,
|
||||
"Spinner": false,
|
||||
"modeType": false,
|
||||
"Idle": false,
|
||||
"serverurl": false,
|
||||
"key": false,
|
||||
"gapi": false,
|
||||
|
|
|
@ -371,4 +371,42 @@ select {
|
|||
.ui-use-tags {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn-mattermost {
|
||||
background-color: #2179ec;
|
||||
border-color: rgba(0,0,0,0.2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-gitlab {
|
||||
background-color: #e35431;
|
||||
border-color: rgba(0,0,0,0.2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-mattermost:hover, .btn-mattermost:active {
|
||||
background-color: #105fc6;
|
||||
border-color: rgba(0,0,0,0.2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-gitlab:hover, .btn-gitlab:active {
|
||||
background-color: #c23b1a;
|
||||
border-color: rgba(0,0,0,0.2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
a.btn.btn-social > i.oauth-icon {
|
||||
display: inline-flex;
|
||||
height: 45px;
|
||||
width: 45px;
|
||||
line-height: inherit;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
a.btn.btn-social > i.oauth-icon > img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg xmlns="http://www.w3.org/2000/svg"
|
||||
width="500" height="500" viewBox="0 0 500 500">
|
||||
<path id="mattermost" fill="#ffffff" stroke="none"
|
||||
d="M 250.05,34.00
|
||||
C 251.95,34.04 253.85,34.11 255.65,34.20
|
||||
255.65,34.20 225.86,69.71 225.86,69.71
|
||||
225.79,69.72 225.71,69.74 225.63,69.75
|
||||
149.26,84.10 98.22,146.50 98.22,222.97
|
||||
98.22,264.53 121.29,313.47 157.97,342.07
|
||||
186.58,364.39 222.26,378.97 259.18,378.97
|
||||
352.58,378.97 419.33,310.36 419.33,222.97
|
||||
419.33,188.06 403.34,150.20 377.57,122.21
|
||||
377.57,122.21 375.94,74.82 375.94,74.82
|
||||
430.39,113.97 465.89,177.84 466.00,249.99
|
||||
466.00,250.00 466.00,250.00 466.00,250.00
|
||||
466.00,369.29 369.30,466.00 250.00,466.00
|
||||
130.71,466.00 34.00,369.29 34.00,250.00
|
||||
34.00,130.71 130.71,34.00 250.00,34.00
|
||||
250.00,34.00 250.05,34.00 250.05,34.00 Z
|
||||
M 314.15,54.29
|
||||
C 314.81,54.25 315.47,54.32 316.11,54.54
|
||||
319.12,55.54 319.96,58.11 320.04,60.99
|
||||
320.04,60.99 323.88,207.87 323.88,207.87
|
||||
324.64,236.53 306.72,276.31 263.49,276.43
|
||||
232.52,276.51 199.81,255.60 199.81,216.30
|
||||
199.82,201.57 205.42,185.04 219.06,168.19
|
||||
219.06,168.19 309.09,57.01 309.09,57.01
|
||||
310.24,55.59 312.17,54.43 314.15,54.29
|
||||
314.15,54.29 314.15,54.29 314.15,54.29 Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -1,37 +1,37 @@
|
|||
/* eslint-env browser, jquery */
|
||||
/* global moment, serverurl */
|
||||
|
||||
import {
|
||||
checkIfAuth,
|
||||
clearLoginState,
|
||||
getLoginState,
|
||||
resetCheckAuth,
|
||||
setloginStateChangeEvent
|
||||
} from './lib/common/login'
|
||||
|
||||
import {
|
||||
clearDuplicatedHistory,
|
||||
deleteServerHistory,
|
||||
getHistory,
|
||||
getStorageHistory,
|
||||
parseHistory,
|
||||
parseServerToHistory,
|
||||
parseStorageToHistory,
|
||||
postHistoryToServer,
|
||||
removeHistory,
|
||||
saveHistory,
|
||||
saveStorageHistoryToServer
|
||||
} from './history'
|
||||
|
||||
import { saveAs } from 'file-saver'
|
||||
import List from 'list.js'
|
||||
import unescapeHTML from 'lodash/unescape'
|
||||
|
||||
require('./locale')
|
||||
|
||||
require('../css/cover.css')
|
||||
require('../css/site.css')
|
||||
|
||||
import {
|
||||
checkIfAuth,
|
||||
clearLoginState,
|
||||
getLoginState,
|
||||
resetCheckAuth,
|
||||
setloginStateChangeEvent
|
||||
} from './lib/common/login'
|
||||
|
||||
import {
|
||||
clearDuplicatedHistory,
|
||||
deleteServerHistory,
|
||||
getHistory,
|
||||
getStorageHistory,
|
||||
parseHistory,
|
||||
parseServerToHistory,
|
||||
parseStorageToHistory,
|
||||
postHistoryToServer,
|
||||
removeHistory,
|
||||
saveHistory,
|
||||
saveStorageHistoryToServer
|
||||
} from './history'
|
||||
|
||||
import { saveAs } from 'file-saver'
|
||||
import List from 'list.js'
|
||||
import S from 'string'
|
||||
|
||||
const options = {
|
||||
valueNames: ['id', 'text', 'timestamp', 'fromNow', 'time', 'tags', 'pinned'],
|
||||
item: `<li class="col-xs-12 col-sm-6 col-md-6 col-lg-4">
|
||||
|
@ -67,27 +67,27 @@ pageInit()
|
|||
|
||||
function pageInit () {
|
||||
checkIfAuth(
|
||||
data => {
|
||||
$('.ui-signin').hide()
|
||||
$('.ui-or').hide()
|
||||
$('.ui-welcome').show()
|
||||
if (data.photo) $('.ui-avatar').prop('src', data.photo).show()
|
||||
else $('.ui-avatar').prop('src', '').hide()
|
||||
$('.ui-name').html(data.name)
|
||||
$('.ui-signout').show()
|
||||
$('.ui-history').click()
|
||||
parseServerToHistory(historyList, parseHistoryCallback)
|
||||
},
|
||||
() => {
|
||||
$('.ui-signin').show()
|
||||
$('.ui-or').show()
|
||||
$('.ui-welcome').hide()
|
||||
$('.ui-avatar').prop('src', '').hide()
|
||||
$('.ui-name').html('')
|
||||
$('.ui-signout').hide()
|
||||
parseStorageToHistory(historyList, parseHistoryCallback)
|
||||
}
|
||||
)
|
||||
data => {
|
||||
$('.ui-signin').hide()
|
||||
$('.ui-or').hide()
|
||||
$('.ui-welcome').show()
|
||||
if (data.photo) $('.ui-avatar').prop('src', data.photo).show()
|
||||
else $('.ui-avatar').prop('src', '').hide()
|
||||
$('.ui-name').html(data.name)
|
||||
$('.ui-signout').show()
|
||||
$('.ui-history').click()
|
||||
parseServerToHistory(historyList, parseHistoryCallback)
|
||||
},
|
||||
() => {
|
||||
$('.ui-signin').show()
|
||||
$('.ui-or').show()
|
||||
$('.ui-welcome').hide()
|
||||
$('.ui-avatar').prop('src', '').hide()
|
||||
$('.ui-name').html('')
|
||||
$('.ui-signout').hide()
|
||||
parseStorageToHistory(historyList, parseHistoryCallback)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
$('.masthead-nav li').click(function () {
|
||||
|
@ -132,7 +132,7 @@ function checkHistoryList () {
|
|||
|
||||
function parseHistoryCallback (list, notehistory) {
|
||||
checkHistoryList()
|
||||
// sort by pinned then timestamp
|
||||
// sort by pinned then timestamp
|
||||
list.sort('', {
|
||||
sortFunction (a, b) {
|
||||
const notea = a.values()
|
||||
|
@ -152,13 +152,13 @@ function parseHistoryCallback (list, notehistory) {
|
|||
}
|
||||
}
|
||||
})
|
||||
// parse filter tags
|
||||
// parse filter tags
|
||||
const filtertags = []
|
||||
for (let i = 0, l = list.items.length; i < l; i++) {
|
||||
const tags = list.items[i]._values.tags
|
||||
if (tags && tags.length > 0) {
|
||||
for (let j = 0; j < tags.length; j++) {
|
||||
// push info filtertags if not found
|
||||
// push info filtertags if not found
|
||||
let found = false
|
||||
if (filtertags.includes(tags[j])) { found = true }
|
||||
if (!found) { filtertags.push(tags[j]) }
|
||||
|
@ -178,20 +178,20 @@ historyList.on('updated', e => {
|
|||
const a = itemEl.find('a')
|
||||
const pin = itemEl.find('.ui-history-pin')
|
||||
const tagsEl = itemEl.find('.tags')
|
||||
// parse link to element a
|
||||
// parse link to element a
|
||||
a.attr('href', `${serverurl}/${values.id}`)
|
||||
// parse pinned
|
||||
// parse pinned
|
||||
if (values.pinned) {
|
||||
pin.addClass('active')
|
||||
} else {
|
||||
pin.removeClass('active')
|
||||
}
|
||||
// parse tags
|
||||
// parse tags
|
||||
const tags = values.tags
|
||||
if (tags && tags.length > 0 && tagsEl.children().length <= 0) {
|
||||
const labels = []
|
||||
for (let j = 0; j < tags.length; j++) {
|
||||
// push into the item label
|
||||
// push into the item label
|
||||
labels.push(`<span class='label label-default'>${tags[j]}</span>`)
|
||||
}
|
||||
tagsEl.html(labels.join(' '))
|
||||
|
@ -328,7 +328,7 @@ $('.ui-open-history').bind('change', e => {
|
|||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const notehistory = JSON.parse(reader.result)
|
||||
// console.log(notehistory);
|
||||
// console.log(notehistory);
|
||||
if (!reader.result) return
|
||||
getHistory(data => {
|
||||
let mergedata = data.concat(notehistory)
|
||||
|
@ -397,7 +397,7 @@ function buildTagsFilter (tags) {
|
|||
for (let i = 0; i < tags.length; i++) {
|
||||
tags[i] = {
|
||||
id: i,
|
||||
text: S(tags[i]).unescapeHTML().s
|
||||
text: unescapeHTML(tags[i])
|
||||
}
|
||||
}
|
||||
filtertags = tags
|
||||
|
|
|
@ -1,6 +1,24 @@
|
|||
/* eslint-env browser, jquery */
|
||||
/* global moment, serverurl */
|
||||
|
||||
import Prism from 'prismjs'
|
||||
import hljs from 'highlight.js'
|
||||
import PDFObject from 'pdfobject'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
import escapeHTML from 'lodash/escape'
|
||||
import unescapeHTML from 'lodash/unescape'
|
||||
|
||||
import { stripTags } from '../../utils/string'
|
||||
|
||||
import getUIElements from './lib/editor/ui-elements'
|
||||
|
||||
import markdownit from 'markdown-it'
|
||||
import markdownitContainer from 'markdown-it-container'
|
||||
|
||||
/* Defined regex markdown it plugins */
|
||||
import Plugin from 'markdown-it-regexp'
|
||||
|
||||
require('prismjs/themes/prism.css')
|
||||
require('prismjs/components/prism-wiki')
|
||||
require('prismjs/components/prism-haskell')
|
||||
|
@ -10,17 +28,9 @@ require('prismjs/components/prism-jsx')
|
|||
require('prismjs/components/prism-makefile')
|
||||
require('prismjs/components/prism-gherkin')
|
||||
|
||||
import Prism from 'prismjs'
|
||||
import hljs from 'highlight.js'
|
||||
import PDFObject from 'pdfobject'
|
||||
import S from 'string'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
require('./lib/common/login')
|
||||
require('../vendor/md-toc')
|
||||
var Viz = require('viz.js')
|
||||
|
||||
import getUIElements from './lib/editor/ui-elements'
|
||||
const ui = getUIElements()
|
||||
|
||||
// auto update last change
|
||||
|
@ -157,7 +167,7 @@ export function renderTags (view) {
|
|||
|
||||
function slugifyWithUTF8 (text) {
|
||||
// remove html tags and trim spaces
|
||||
let newText = S(text).trim().stripTags().s
|
||||
let newText = stripTags(text.toString().trim())
|
||||
// replace all spaces in between to dashes
|
||||
newText = newText.replace(/\s+/g, '-')
|
||||
// slugify string to make it valid for attribute
|
||||
|
@ -190,7 +200,7 @@ export function parseMeta (md, edit, view, toc, tocAffix) {
|
|||
dir = meta.dir
|
||||
breaks = meta.breaks
|
||||
}
|
||||
// text language
|
||||
// text language
|
||||
if (lang && typeof lang === 'string') {
|
||||
view.attr('lang', lang)
|
||||
toc.attr('lang', lang)
|
||||
|
@ -202,7 +212,7 @@ export function parseMeta (md, edit, view, toc, tocAffix) {
|
|||
tocAffix.removeAttr('lang')
|
||||
if (edit) { edit.removeAttr('lang', lang) }
|
||||
}
|
||||
// text direction
|
||||
// text direction
|
||||
if (dir && typeof dir === 'string') {
|
||||
view.attr('dir', dir)
|
||||
toc.attr('dir', dir)
|
||||
|
@ -212,7 +222,7 @@ export function parseMeta (md, edit, view, toc, tocAffix) {
|
|||
toc.removeAttr('dir')
|
||||
tocAffix.removeAttr('dir')
|
||||
}
|
||||
// breaks
|
||||
// breaks
|
||||
if (typeof breaks === 'boolean' && !breaks) {
|
||||
md.options.breaks = false
|
||||
} else {
|
||||
|
@ -245,7 +255,7 @@ if (typeof window.mermaid !== 'undefined' && window.mermaid) window.mermaid.star
|
|||
|
||||
// dynamic event or object binding here
|
||||
export function finishView (view) {
|
||||
// todo list
|
||||
// todo list
|
||||
const lis = view.find('li.raw').removeClass('raw').sortByDepth().toArray()
|
||||
|
||||
for (let li of lis) {
|
||||
|
@ -259,9 +269,9 @@ export function finishView (view) {
|
|||
li.innerHTML = html
|
||||
let disabled = 'disabled'
|
||||
if (typeof editor !== 'undefined' && window.havePermission()) { disabled = '' }
|
||||
if (/^\s*\[[x ]\]\s*/.test(html)) {
|
||||
li.innerHTML = html.replace(/^\s*\[ \]\s*/, `<input type="checkbox" class="task-list-item-checkbox "${disabled}><label></label>`)
|
||||
.replace(/^\s*\[x\]\s*/, `<input type="checkbox" class="task-list-item-checkbox" checked ${disabled}><label></label>`)
|
||||
if (/^\s*\[[x ]]\s*/.test(html)) {
|
||||
li.innerHTML = html.replace(/^\s*\[ ]\s*/, `<input type="checkbox" class="task-list-item-checkbox "${disabled}><label></label>`)
|
||||
.replace(/^\s*\[x]\s*/, `<input type="checkbox" class="task-list-item-checkbox" checked ${disabled}><label></label>`)
|
||||
if (li.tagName.toLowerCase() !== 'li') {
|
||||
li.parentElement.setAttribute('class', 'task-list-item')
|
||||
} else {
|
||||
|
@ -269,42 +279,42 @@ export function finishView (view) {
|
|||
}
|
||||
}
|
||||
if (typeof editor !== 'undefined' && window.havePermission()) { $(li).find('input').change(toggleTodoEvent) }
|
||||
// color tag in list will convert it to tag icon with color
|
||||
// color tag in list will convert it to tag icon with color
|
||||
const tagColor = $(li).closest('ul').find('.color')
|
||||
tagColor.each((key, value) => {
|
||||
$(value).addClass('fa fa-tag').css('color', $(value).attr('data-color'))
|
||||
})
|
||||
}
|
||||
|
||||
// youtube
|
||||
// youtube
|
||||
view.find('div.youtube.raw').removeClass('raw')
|
||||
.click(function () {
|
||||
imgPlayiframe(this, '//www.youtube.com/embed/')
|
||||
})
|
||||
.click(function () {
|
||||
imgPlayiframe(this, '//www.youtube.com/embed/')
|
||||
})
|
||||
// vimeo
|
||||
view.find('div.vimeo.raw').removeClass('raw')
|
||||
.click(function () {
|
||||
imgPlayiframe(this, '//player.vimeo.com/video/')
|
||||
})
|
||||
.each((key, value) => {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: `//vimeo.com/api/v2/video/${$(value).attr('data-videoid')}.json`,
|
||||
jsonp: 'callback',
|
||||
dataType: 'jsonp',
|
||||
success (data) {
|
||||
const thumbnailSrc = data[0].thumbnail_large
|
||||
const image = `<img src="${thumbnailSrc}" />`
|
||||
$(value).prepend(image)
|
||||
if (window.viewAjaxCallback) window.viewAjaxCallback()
|
||||
}
|
||||
})
|
||||
})
|
||||
.click(function () {
|
||||
imgPlayiframe(this, '//player.vimeo.com/video/')
|
||||
})
|
||||
.each((key, value) => {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: `//vimeo.com/api/v2/video/${$(value).attr('data-videoid')}.json`,
|
||||
jsonp: 'callback',
|
||||
dataType: 'jsonp',
|
||||
success (data) {
|
||||
const thumbnailSrc = data[0].thumbnail_large
|
||||
const image = `<img src="${thumbnailSrc}" />`
|
||||
$(value).prepend(image)
|
||||
if (window.viewAjaxCallback) window.viewAjaxCallback()
|
||||
}
|
||||
})
|
||||
})
|
||||
// gist
|
||||
view.find('code[data-gist-id]').each((key, value) => {
|
||||
if ($(value).children().length === 0) { $(value).gist(window.viewAjaxCallback) }
|
||||
})
|
||||
// sequence diagram
|
||||
// sequence diagram
|
||||
const sequences = view.find('div.sequence-diagram.raw').removeClass('raw')
|
||||
sequences.each((key, value) => {
|
||||
try {
|
||||
|
@ -323,11 +333,11 @@ export function finishView (view) {
|
|||
svg[0].setAttribute('preserveAspectRatio', 'xMidYMid meet')
|
||||
} catch (err) {
|
||||
$value.unwrap()
|
||||
$value.parent().append('<div class="alert alert-warning">' + err + '</div>')
|
||||
$value.parent().append(`<div class="alert alert-warning">${escapeHTML(err)}</div>`)
|
||||
console.warn(err)
|
||||
}
|
||||
})
|
||||
// flowchart
|
||||
// flowchart
|
||||
const flow = view.find('div.flow-chart.raw').removeClass('raw')
|
||||
flow.each((key, value) => {
|
||||
try {
|
||||
|
@ -347,11 +357,11 @@ export function finishView (view) {
|
|||
$value.children().unwrap().unwrap()
|
||||
} catch (err) {
|
||||
$value.unwrap()
|
||||
$value.parent().append('<div class="alert alert-warning">' + err + '</div>')
|
||||
$value.parent().append(`<div class="alert alert-warning">${escapeHTML(err)}</div>`)
|
||||
console.warn(err)
|
||||
}
|
||||
})
|
||||
// graphviz
|
||||
// graphviz
|
||||
var graphvizs = view.find('div.graphviz.raw').removeClass('raw')
|
||||
graphvizs.each(function (key, value) {
|
||||
try {
|
||||
|
@ -366,11 +376,11 @@ export function finishView (view) {
|
|||
$value.children().unwrap().unwrap()
|
||||
} catch (err) {
|
||||
$value.unwrap()
|
||||
$value.parent().append('<div class="alert alert-warning">' + err + '</div>')
|
||||
$value.parent().append(`<div class="alert alert-warning">${escapeHTML(err)}</div>`)
|
||||
console.warn(err)
|
||||
}
|
||||
})
|
||||
// mermaid
|
||||
// mermaid
|
||||
const mermaids = view.find('div.mermaid.raw').removeClass('raw')
|
||||
mermaids.each((key, value) => {
|
||||
try {
|
||||
|
@ -388,7 +398,7 @@ export function finishView (view) {
|
|||
}
|
||||
|
||||
$value.unwrap()
|
||||
$value.parent().append('<div class="alert alert-warning">' + errormessage + '</div>')
|
||||
$value.parent().append(`<div class="alert alert-warning">${escapeHTML(errormessage)}</div>`)
|
||||
console.warn(errormessage)
|
||||
}
|
||||
})
|
||||
|
@ -408,20 +418,20 @@ export function finishView (view) {
|
|||
svg[0].setAttribute('preserveAspectRatio', 'xMidYMid meet')
|
||||
} catch (err) {
|
||||
$value.unwrap()
|
||||
$value.parent().append('<div class="alert alert-warning">' + err + '</div>')
|
||||
$value.parent().append(`<div class="alert alert-warning">${escapeHTML(err)}</div>`)
|
||||
console.warn(err)
|
||||
}
|
||||
})
|
||||
// image href new window(emoji not included)
|
||||
// image href new window(emoji not included)
|
||||
const images = view.find('img.raw[src]').removeClass('raw')
|
||||
images.each((key, value) => {
|
||||
// if it's already wrapped by link, then ignore
|
||||
// if it's already wrapped by link, then ignore
|
||||
const $value = $(value)
|
||||
$value[0].onload = e => {
|
||||
if (window.viewAjaxCallback) window.viewAjaxCallback()
|
||||
}
|
||||
})
|
||||
// blockquote
|
||||
// blockquote
|
||||
const blockquote = view.find('blockquote.raw').removeClass('raw')
|
||||
const blockquoteP = blockquote.find('p')
|
||||
blockquoteP.each((key, value) => {
|
||||
|
@ -429,96 +439,96 @@ export function finishView (view) {
|
|||
html = replaceExtraTags(html)
|
||||
$(value).html(html)
|
||||
})
|
||||
// color tag in blockquote will change its left border color
|
||||
// color tag in blockquote will change its left border color
|
||||
const blockquoteColor = blockquote.find('.color')
|
||||
blockquoteColor.each((key, value) => {
|
||||
$(value).closest('blockquote').css('border-left-color', $(value).attr('data-color'))
|
||||
})
|
||||
// slideshare
|
||||
// slideshare
|
||||
view.find('div.slideshare.raw').removeClass('raw')
|
||||
.each((key, value) => {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: `//www.slideshare.net/api/oembed/2?url=http://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`,
|
||||
jsonp: 'callback',
|
||||
dataType: 'jsonp',
|
||||
success (data) {
|
||||
const $html = $(data.html)
|
||||
const iframe = $html.closest('iframe')
|
||||
const caption = $html.closest('div')
|
||||
const inner = $('<div class="inner"></div>').append(iframe)
|
||||
const height = iframe.attr('height')
|
||||
const width = iframe.attr('width')
|
||||
const ratio = (height / width) * 100
|
||||
inner.css('padding-bottom', `${ratio}%`)
|
||||
$(value).html(inner).append(caption)
|
||||
if (window.viewAjaxCallback) window.viewAjaxCallback()
|
||||
}
|
||||
})
|
||||
})
|
||||
.each((key, value) => {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: `//www.slideshare.net/api/oembed/2?url=http://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`,
|
||||
jsonp: 'callback',
|
||||
dataType: 'jsonp',
|
||||
success (data) {
|
||||
const $html = $(data.html)
|
||||
const iframe = $html.closest('iframe')
|
||||
const caption = $html.closest('div')
|
||||
const inner = $('<div class="inner"></div>').append(iframe)
|
||||
const height = iframe.attr('height')
|
||||
const width = iframe.attr('width')
|
||||
const ratio = (height / width) * 100
|
||||
inner.css('padding-bottom', `${ratio}%`)
|
||||
$(value).html(inner).append(caption)
|
||||
if (window.viewAjaxCallback) window.viewAjaxCallback()
|
||||
}
|
||||
})
|
||||
})
|
||||
// speakerdeck
|
||||
view.find('div.speakerdeck.raw').removeClass('raw')
|
||||
.each((key, value) => {
|
||||
const url = `https://speakerdeck.com/${$(value).attr('data-speakerdeckid')}`
|
||||
const inner = $('<a>Speakerdeck</a>')
|
||||
inner.attr('href', url)
|
||||
inner.attr('rel', 'noopener noreferrer')
|
||||
inner.attr('target', '_blank')
|
||||
$(value).append(inner)
|
||||
})
|
||||
.each((key, value) => {
|
||||
const url = `https://speakerdeck.com/${$(value).attr('data-speakerdeckid')}`
|
||||
const inner = $('<a>Speakerdeck</a>')
|
||||
inner.attr('href', url)
|
||||
inner.attr('rel', 'noopener noreferrer')
|
||||
inner.attr('target', '_blank')
|
||||
$(value).append(inner)
|
||||
})
|
||||
// pdf
|
||||
view.find('div.pdf.raw').removeClass('raw')
|
||||
.each(function (key, value) {
|
||||
const url = $(value).attr('data-pdfurl')
|
||||
const inner = $('<div></div>')
|
||||
$(this).append(inner)
|
||||
PDFObject.embed(url, inner, {
|
||||
height: '400px'
|
||||
})
|
||||
})
|
||||
.each(function (key, value) {
|
||||
const url = $(value).attr('data-pdfurl')
|
||||
const inner = $('<div></div>')
|
||||
$(this).append(inner)
|
||||
PDFObject.embed(url, inner, {
|
||||
height: '400px'
|
||||
})
|
||||
})
|
||||
// syntax highlighting
|
||||
view.find('code.raw').removeClass('raw')
|
||||
.each((key, value) => {
|
||||
const langDiv = $(value)
|
||||
if (langDiv.length > 0) {
|
||||
const reallang = langDiv[0].className.replace(/hljs|wrap/g, '').trim()
|
||||
const codeDiv = langDiv.find('.code')
|
||||
let code = ''
|
||||
if (codeDiv.length > 0) code = codeDiv.html()
|
||||
else code = langDiv.html()
|
||||
var result
|
||||
if (!reallang) {
|
||||
result = {
|
||||
value: code
|
||||
}
|
||||
} else if (reallang === 'haskell' || reallang === 'go' || reallang === 'typescript' || reallang === 'jsx' || reallang === 'gherkin') {
|
||||
code = S(code).unescapeHTML().s
|
||||
result = {
|
||||
value: Prism.highlight(code, Prism.languages[reallang])
|
||||
}
|
||||
} else if (reallang === 'tiddlywiki' || reallang === 'mediawiki') {
|
||||
code = S(code).unescapeHTML().s
|
||||
result = {
|
||||
value: Prism.highlight(code, Prism.languages.wiki)
|
||||
}
|
||||
} else if (reallang === 'cmake') {
|
||||
code = S(code).unescapeHTML().s
|
||||
result = {
|
||||
value: Prism.highlight(code, Prism.languages.makefile)
|
||||
}
|
||||
} else {
|
||||
code = S(code).unescapeHTML().s
|
||||
const languages = hljs.listLanguages()
|
||||
if (!languages.includes(reallang)) {
|
||||
result = hljs.highlightAuto(code)
|
||||
} else {
|
||||
result = hljs.highlight(reallang, code)
|
||||
}
|
||||
}
|
||||
if (codeDiv.length > 0) codeDiv.html(result.value)
|
||||
else langDiv.html(result.value)
|
||||
.each((key, value) => {
|
||||
const langDiv = $(value)
|
||||
if (langDiv.length > 0) {
|
||||
const reallang = langDiv[0].className.replace(/hljs|wrap/g, '').trim()
|
||||
const codeDiv = langDiv.find('.code')
|
||||
let code = ''
|
||||
if (codeDiv.length > 0) code = codeDiv.html()
|
||||
else code = langDiv.html()
|
||||
var result
|
||||
if (!reallang) {
|
||||
result = {
|
||||
value: code
|
||||
}
|
||||
})
|
||||
} else if (reallang === 'haskell' || reallang === 'go' || reallang === 'typescript' || reallang === 'jsx' || reallang === 'gherkin') {
|
||||
code = unescapeHTML(code)
|
||||
result = {
|
||||
value: Prism.highlight(code, Prism.languages[reallang])
|
||||
}
|
||||
} else if (reallang === 'tiddlywiki' || reallang === 'mediawiki') {
|
||||
code = unescapeHTML(code)
|
||||
result = {
|
||||
value: Prism.highlight(code, Prism.languages.wiki)
|
||||
}
|
||||
} else if (reallang === 'cmake') {
|
||||
code = unescapeHTML(code)
|
||||
result = {
|
||||
value: Prism.highlight(code, Prism.languages.makefile)
|
||||
}
|
||||
} else {
|
||||
code = unescapeHTML(code)
|
||||
const languages = hljs.listLanguages()
|
||||
if (!languages.includes(reallang)) {
|
||||
result = hljs.highlightAuto(code)
|
||||
} else {
|
||||
result = hljs.highlight(reallang, code)
|
||||
}
|
||||
}
|
||||
if (codeDiv.length > 0) codeDiv.html(result.value)
|
||||
else langDiv.html(result.value)
|
||||
}
|
||||
})
|
||||
// mathjax
|
||||
const mathjaxdivs = view.find('span.mathjax.raw').removeClass('raw').toArray()
|
||||
try {
|
||||
|
@ -532,7 +542,7 @@ export function finishView (view) {
|
|||
} catch (err) {
|
||||
console.warn(err)
|
||||
}
|
||||
// render title
|
||||
// render title
|
||||
document.title = renderTitle(view)
|
||||
}
|
||||
|
||||
|
@ -568,7 +578,7 @@ export function postProcess (code) {
|
|||
if (warning && warning.length > 0) {
|
||||
warning.text(md.metaError)
|
||||
} else {
|
||||
warning = $('<div id="meta-error" class="alert alert-warning">' + md.metaError + '</div>')
|
||||
warning = $(`<div id="meta-error" class="alert alert-warning">${escapeHTML(md.metaError)}</div>`)
|
||||
result.prepend(warning)
|
||||
}
|
||||
}
|
||||
|
@ -592,23 +602,23 @@ window.removeDOMEvents = removeDOMEvents
|
|||
function generateCleanHTML (view) {
|
||||
const src = view.clone()
|
||||
const eles = src.find('*')
|
||||
// remove syncscroll parts
|
||||
// remove syncscroll parts
|
||||
eles.removeClass('part')
|
||||
src.find('*[class=""]').removeAttr('class')
|
||||
eles.removeAttr('data-startline data-endline')
|
||||
src.find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll')
|
||||
// remove gist content
|
||||
// remove gist content
|
||||
src.find('code[data-gist-id]').children().remove()
|
||||
// disable todo list
|
||||
// disable todo list
|
||||
src.find('input.task-list-item-checkbox').attr('disabled', '')
|
||||
// replace emoji image path
|
||||
// replace emoji image path
|
||||
src.find('img.emoji').each((key, value) => {
|
||||
let name = $(value).attr('alt')
|
||||
name = name.substr(1)
|
||||
name = name.slice(0, name.length - 1)
|
||||
$(value).attr('src', `https://cdnjs.cloudflare.com/ajax/libs/emojify.js/1.1.0/images/basic/${name}.png`)
|
||||
})
|
||||
// replace video to iframe
|
||||
// replace video to iframe
|
||||
src.find('div[data-videoid]').each((key, value) => {
|
||||
const id = $(value).attr('data-videoid')
|
||||
const style = $(value).attr('style')
|
||||
|
@ -644,12 +654,12 @@ export function exportToHTML (view) {
|
|||
const title = renderTitle(ui.area.markdown)
|
||||
const filename = `${renderFilename(ui.area.markdown)}.html`
|
||||
const src = generateCleanHTML(view)
|
||||
// generate toc
|
||||
// generate toc
|
||||
const toc = $('#ui-toc').clone()
|
||||
toc.find('*').removeClass('active').find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll')
|
||||
const tocAffix = $('#ui-toc-affix').clone()
|
||||
tocAffix.find('*').removeClass('active').find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll')
|
||||
// generate html via template
|
||||
// generate html via template
|
||||
$.get(`${serverurl}/build/html.min.css`, css => {
|
||||
$.get(`${serverurl}/views/html.hbs`, data => {
|
||||
const template = window.Handlebars.compile(data)
|
||||
|
@ -664,7 +674,7 @@ export function exportToHTML (view) {
|
|||
dir: (md && md.meta && md.meta.dir) ? `dir="${md.meta.dir}"` : null
|
||||
}
|
||||
const html = template(context)
|
||||
// console.log(html);
|
||||
// console.log(html);
|
||||
const blob = new Blob([html], {
|
||||
type: 'text/html;charset=utf-8'
|
||||
})
|
||||
|
@ -779,20 +789,20 @@ export function smoothHashScroll () {
|
|||
const hash = element.hash
|
||||
if (hash) {
|
||||
$element.on('click', function (e) {
|
||||
// store hash
|
||||
// store hash
|
||||
const hash = decodeURIComponent(this.hash)
|
||||
// escape special characters in jquery selector
|
||||
// escape special characters in jquery selector
|
||||
const $hash = $(hash.replace(/(:|\.|\[|\]|,)/g, '\\$1'))
|
||||
// return if no element been selected
|
||||
// return if no element been selected
|
||||
if ($hash.length <= 0) return
|
||||
// prevent default anchor click behavior
|
||||
// prevent default anchor click behavior
|
||||
e.preventDefault()
|
||||
// animate
|
||||
// animate
|
||||
$('body, html').stop(true, true).animate({
|
||||
scrollTop: $hash.offset().top
|
||||
}, 100, 'linear', () => {
|
||||
// when done, add hash to url
|
||||
// (default click behaviour)
|
||||
// when done, add hash to url
|
||||
// (default click behaviour)
|
||||
window.location.hash = hash
|
||||
})
|
||||
})
|
||||
|
@ -902,7 +912,7 @@ export function scrollToHash () {
|
|||
|
||||
function highlightRender (code, lang) {
|
||||
if (!lang || /no(-?)highlight|plain|text/.test(lang)) { return }
|
||||
code = S(code).escapeHTML().s
|
||||
code = escapeHTML(code)
|
||||
if (lang === 'sequence') {
|
||||
return `<div class="sequence-diagram raw">${code}</div>`
|
||||
} else if (lang === 'flow') {
|
||||
|
@ -934,9 +944,6 @@ function highlightRender (code, lang) {
|
|||
return result.value
|
||||
}
|
||||
|
||||
import markdownit from 'markdown-it'
|
||||
import markdownitContainer from 'markdown-it-container'
|
||||
|
||||
export let md = markdownit('default', {
|
||||
html: true,
|
||||
breaks: true,
|
||||
|
@ -1034,109 +1041,106 @@ md.renderer.rules.fence = (tokens, idx, options, env, self) => {
|
|||
return `<pre><code${self.renderAttrs(token)}>${highlighted}</code></pre>\n`
|
||||
}
|
||||
|
||||
/* Defined regex markdown it plugins */
|
||||
import Plugin from 'markdown-it-regexp'
|
||||
|
||||
// youtube
|
||||
const youtubePlugin = new Plugin(
|
||||
// regexp to match
|
||||
/{%youtube\s*([\d\D]*?)\s*%}/,
|
||||
// regexp to match
|
||||
/{%youtube\s*([\d\D]*?)\s*%}/,
|
||||
|
||||
(match, utils) => {
|
||||
const videoid = match[1]
|
||||
if (!videoid) return
|
||||
const div = $('<div class="youtube raw"></div>')
|
||||
div.attr('data-videoid', videoid)
|
||||
const thumbnailSrc = `//img.youtube.com/vi/${videoid}/hqdefault.jpg`
|
||||
const image = `<img src="${thumbnailSrc}" />`
|
||||
div.append(image)
|
||||
const icon = '<i class="icon fa fa-youtube-play fa-5x"></i>'
|
||||
div.append(icon)
|
||||
return div[0].outerHTML
|
||||
}
|
||||
(match, utils) => {
|
||||
const videoid = match[1]
|
||||
if (!videoid) return
|
||||
const div = $('<div class="youtube raw"></div>')
|
||||
div.attr('data-videoid', videoid)
|
||||
const thumbnailSrc = `//img.youtube.com/vi/${videoid}/hqdefault.jpg`
|
||||
const image = `<img src="${thumbnailSrc}" />`
|
||||
div.append(image)
|
||||
const icon = '<i class="icon fa fa-youtube-play fa-5x"></i>'
|
||||
div.append(icon)
|
||||
return div[0].outerHTML
|
||||
}
|
||||
)
|
||||
// vimeo
|
||||
const vimeoPlugin = new Plugin(
|
||||
// regexp to match
|
||||
/{%vimeo\s*([\d\D]*?)\s*%}/,
|
||||
// regexp to match
|
||||
/{%vimeo\s*([\d\D]*?)\s*%}/,
|
||||
|
||||
(match, utils) => {
|
||||
const videoid = match[1]
|
||||
if (!videoid) return
|
||||
const div = $('<div class="vimeo raw"></div>')
|
||||
div.attr('data-videoid', videoid)
|
||||
const icon = '<i class="icon fa fa-vimeo-square fa-5x"></i>'
|
||||
div.append(icon)
|
||||
return div[0].outerHTML
|
||||
}
|
||||
(match, utils) => {
|
||||
const videoid = match[1]
|
||||
if (!videoid) return
|
||||
const div = $('<div class="vimeo raw"></div>')
|
||||
div.attr('data-videoid', videoid)
|
||||
const icon = '<i class="icon fa fa-vimeo-square fa-5x"></i>'
|
||||
div.append(icon)
|
||||
return div[0].outerHTML
|
||||
}
|
||||
)
|
||||
// gist
|
||||
const gistPlugin = new Plugin(
|
||||
// regexp to match
|
||||
/{%gist\s*([\d\D]*?)\s*%}/,
|
||||
// regexp to match
|
||||
/{%gist\s*([\d\D]*?)\s*%}/,
|
||||
|
||||
(match, utils) => {
|
||||
const gistid = match[1]
|
||||
const code = `<code data-gist-id="${gistid}"></code>`
|
||||
return code
|
||||
}
|
||||
(match, utils) => {
|
||||
const gistid = match[1]
|
||||
const code = `<code data-gist-id="${gistid}"></code>`
|
||||
return code
|
||||
}
|
||||
)
|
||||
// TOC
|
||||
const tocPlugin = new Plugin(
|
||||
// regexp to match
|
||||
/^\[TOC\]$/i,
|
||||
// regexp to match
|
||||
/^\[TOC\]$/i,
|
||||
|
||||
(match, utils) => '<div class="toc"></div>'
|
||||
(match, utils) => '<div class="toc"></div>'
|
||||
)
|
||||
// slideshare
|
||||
const slidesharePlugin = new Plugin(
|
||||
// regexp to match
|
||||
/{%slideshare\s*([\d\D]*?)\s*%}/,
|
||||
// regexp to match
|
||||
/{%slideshare\s*([\d\D]*?)\s*%}/,
|
||||
|
||||
(match, utils) => {
|
||||
const slideshareid = match[1]
|
||||
const div = $('<div class="slideshare raw"></div>')
|
||||
div.attr('data-slideshareid', slideshareid)
|
||||
return div[0].outerHTML
|
||||
}
|
||||
(match, utils) => {
|
||||
const slideshareid = match[1]
|
||||
const div = $('<div class="slideshare raw"></div>')
|
||||
div.attr('data-slideshareid', slideshareid)
|
||||
return div[0].outerHTML
|
||||
}
|
||||
)
|
||||
// speakerdeck
|
||||
const speakerdeckPlugin = new Plugin(
|
||||
// regexp to match
|
||||
/{%speakerdeck\s*([\d\D]*?)\s*%}/,
|
||||
// regexp to match
|
||||
/{%speakerdeck\s*([\d\D]*?)\s*%}/,
|
||||
|
||||
(match, utils) => {
|
||||
const speakerdeckid = match[1]
|
||||
const div = $('<div class="speakerdeck raw"></div>')
|
||||
div.attr('data-speakerdeckid', speakerdeckid)
|
||||
return div[0].outerHTML
|
||||
}
|
||||
(match, utils) => {
|
||||
const speakerdeckid = match[1]
|
||||
const div = $('<div class="speakerdeck raw"></div>')
|
||||
div.attr('data-speakerdeckid', speakerdeckid)
|
||||
return div[0].outerHTML
|
||||
}
|
||||
)
|
||||
// pdf
|
||||
const pdfPlugin = new Plugin(
|
||||
// regexp to match
|
||||
/{%pdf\s*([\d\D]*?)\s*%}/,
|
||||
// regexp to match
|
||||
/{%pdf\s*([\d\D]*?)\s*%}/,
|
||||
|
||||
(match, utils) => {
|
||||
const pdfurl = match[1]
|
||||
if (!isValidURL(pdfurl)) return match[0]
|
||||
const div = $('<div class="pdf raw"></div>')
|
||||
div.attr('data-pdfurl', pdfurl)
|
||||
return div[0].outerHTML
|
||||
}
|
||||
(match, utils) => {
|
||||
const pdfurl = match[1]
|
||||
if (!isValidURL(pdfurl)) return match[0]
|
||||
const div = $('<div class="pdf raw"></div>')
|
||||
div.attr('data-pdfurl', pdfurl)
|
||||
return div[0].outerHTML
|
||||
}
|
||||
)
|
||||
|
||||
const emojijsPlugin = new Plugin(
|
||||
// regexp to match emoji shortcodes :something:
|
||||
// We generate an universal regex that guaranteed only contains the
|
||||
// emojies we have available. This should prevent all false-positives
|
||||
new RegExp(':(' + window.emojify.emojiNames.map((item) => { return RegExp.escape(item) }).join('|') + '):', 'i'),
|
||||
// regexp to match emoji shortcodes :something:
|
||||
// We generate an universal regex that guaranteed only contains the
|
||||
// emojies we have available. This should prevent all false-positives
|
||||
new RegExp(':(' + window.emojify.emojiNames.map((item) => { return RegExp.escape(item) }).join('|') + '):', 'i'),
|
||||
|
||||
(match, utils) => {
|
||||
const emoji = match[1].toLowerCase()
|
||||
const div = $(`<img class="emoji" alt=":${emoji}:" src="${serverurl}/build/emojify.js/dist/images/basic/${emoji}.png"></img>`)
|
||||
return div[0].outerHTML
|
||||
}
|
||||
(match, utils) => {
|
||||
const emoji = match[1].toLowerCase()
|
||||
const div = $(`<img class="emoji" alt=":${emoji}:" src="${serverurl}/build/emojify.js/dist/images/basic/${emoji}.png"></img>`)
|
||||
return div[0].outerHTML
|
||||
}
|
||||
)
|
||||
|
||||
// yaml meta, from https://github.com/eugeneware/remarkable-meta
|
||||
|
|
|
@ -2,65 +2,64 @@
|
|||
/* global serverurl, moment */
|
||||
|
||||
import store from 'store'
|
||||
import S from 'string'
|
||||
import LZString from 'lz-string'
|
||||
import LZString from '@hackmd/lz-string'
|
||||
|
||||
import escapeHTML from 'lodash/escape'
|
||||
|
||||
import wurl from 'wurl'
|
||||
|
||||
import {
|
||||
checkNoteIdValid,
|
||||
encodeNoteId
|
||||
} from './utils'
|
||||
|
||||
import {
|
||||
checkIfAuth
|
||||
} from './lib/common/login'
|
||||
import { checkIfAuth } from './lib/common/login'
|
||||
|
||||
import {
|
||||
urlpath
|
||||
} from './lib/config'
|
||||
import { urlpath } from './lib/config'
|
||||
|
||||
window.migrateHistoryFromTempCallback = null
|
||||
|
||||
migrateHistoryFromTemp()
|
||||
|
||||
function migrateHistoryFromTemp () {
|
||||
if (window.url('#tempid')) {
|
||||
if (wurl('#tempid')) {
|
||||
$.get(`${serverurl}/temp`, {
|
||||
tempid: window.url('#tempid')
|
||||
tempid: wurl('#tempid')
|
||||
})
|
||||
.done(data => {
|
||||
if (data && data.temp) {
|
||||
getStorageHistory(olddata => {
|
||||
if (!olddata || olddata.length === 0) {
|
||||
saveHistoryToStorage(JSON.parse(data.temp))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.always(() => {
|
||||
let hash = location.hash.split('#')[1]
|
||||
hash = hash.split('&')
|
||||
for (let i = 0; i < hash.length; i++) {
|
||||
if (hash[i].indexOf('tempid') === 0) {
|
||||
hash.splice(i, 1)
|
||||
i--
|
||||
.done(data => {
|
||||
if (data && data.temp) {
|
||||
getStorageHistory(olddata => {
|
||||
if (!olddata || olddata.length === 0) {
|
||||
saveHistoryToStorage(JSON.parse(data.temp))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
hash = hash.join('&')
|
||||
location.hash = hash
|
||||
if (window.migrateHistoryFromTempCallback) { window.migrateHistoryFromTempCallback() }
|
||||
})
|
||||
})
|
||||
.always(() => {
|
||||
let hash = location.hash.split('#')[1]
|
||||
hash = hash.split('&')
|
||||
for (let i = 0; i < hash.length; i++) {
|
||||
if (hash[i].indexOf('tempid') === 0) {
|
||||
hash.splice(i, 1)
|
||||
i--
|
||||
}
|
||||
}
|
||||
hash = hash.join('&')
|
||||
location.hash = hash
|
||||
if (window.migrateHistoryFromTempCallback) { window.migrateHistoryFromTempCallback() }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function saveHistory (notehistory) {
|
||||
checkIfAuth(
|
||||
() => {
|
||||
saveHistoryToServer(notehistory)
|
||||
},
|
||||
() => {
|
||||
saveHistoryToStorage(notehistory)
|
||||
}
|
||||
)
|
||||
() => {
|
||||
saveHistoryToServer(notehistory)
|
||||
},
|
||||
() => {
|
||||
saveHistoryToStorage(notehistory)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function saveHistoryToStorage (notehistory) {
|
||||
|
@ -79,9 +78,9 @@ export function saveStorageHistoryToServer (callback) {
|
|||
$.post(`${serverurl}/history`, {
|
||||
history: data
|
||||
})
|
||||
.done(data => {
|
||||
callback(data)
|
||||
})
|
||||
.done(data => {
|
||||
callback(data)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,7 +107,7 @@ export function clearDuplicatedHistory (notehistory) {
|
|||
}
|
||||
|
||||
function addHistory (id, text, time, tags, pinned, notehistory) {
|
||||
// only add when note id exists
|
||||
// only add when note id exists
|
||||
if (id) {
|
||||
notehistory.push({
|
||||
id,
|
||||
|
@ -134,14 +133,14 @@ export function removeHistory (id, notehistory) {
|
|||
// used for inner
|
||||
export function writeHistory (title, tags) {
|
||||
checkIfAuth(
|
||||
() => {
|
||||
// no need to do this anymore, this will count from server-side
|
||||
// writeHistoryToServer(title, tags);
|
||||
},
|
||||
() => {
|
||||
writeHistoryToStorage(title, tags)
|
||||
}
|
||||
)
|
||||
() => {
|
||||
// no need to do this anymore, this will count from server-side
|
||||
// writeHistoryToServer(title, tags);
|
||||
},
|
||||
() => {
|
||||
writeHistoryToStorage(title, tags)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function writeHistoryToStorage (title, tags) {
|
||||
|
@ -162,7 +161,7 @@ if (!Array.isArray) {
|
|||
}
|
||||
|
||||
function renderHistory (title, tags) {
|
||||
// console.debug(tags);
|
||||
// console.debug(tags);
|
||||
const id = urlpath ? location.pathname.slice(urlpath.length + 1, location.pathname.length).split('/')[1] : location.pathname.split('/')[1]
|
||||
return {
|
||||
id,
|
||||
|
@ -174,7 +173,7 @@ function renderHistory (title, tags) {
|
|||
|
||||
function generateHistory (title, tags, notehistory) {
|
||||
const info = renderHistory(title, tags)
|
||||
// keep any pinned data
|
||||
// keep any pinned data
|
||||
let pinned = false
|
||||
for (let i = 0; i < notehistory.length; i++) {
|
||||
if (notehistory[i].id === info.id && notehistory[i].pinned) {
|
||||
|
@ -191,25 +190,25 @@ function generateHistory (title, tags, notehistory) {
|
|||
// used for outer
|
||||
export function getHistory (callback) {
|
||||
checkIfAuth(
|
||||
() => {
|
||||
getServerHistory(callback)
|
||||
},
|
||||
() => {
|
||||
getStorageHistory(callback)
|
||||
}
|
||||
)
|
||||
() => {
|
||||
getServerHistory(callback)
|
||||
},
|
||||
() => {
|
||||
getStorageHistory(callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function getServerHistory (callback) {
|
||||
$.get(`${serverurl}/history`)
|
||||
.done(data => {
|
||||
if (data.history) {
|
||||
callback(data.history)
|
||||
}
|
||||
})
|
||||
.fail((xhr, status, error) => {
|
||||
console.error(xhr.responseText)
|
||||
})
|
||||
.done(data => {
|
||||
if (data.history) {
|
||||
callback(data.history)
|
||||
}
|
||||
})
|
||||
.fail((xhr, status, error) => {
|
||||
console.error(xhr.responseText)
|
||||
})
|
||||
}
|
||||
|
||||
export function getStorageHistory (callback) {
|
||||
|
@ -224,25 +223,25 @@ export function getStorageHistory (callback) {
|
|||
|
||||
export function parseHistory (list, callback) {
|
||||
checkIfAuth(
|
||||
() => {
|
||||
parseServerToHistory(list, callback)
|
||||
},
|
||||
() => {
|
||||
parseStorageToHistory(list, callback)
|
||||
}
|
||||
)
|
||||
() => {
|
||||
parseServerToHistory(list, callback)
|
||||
},
|
||||
() => {
|
||||
parseStorageToHistory(list, callback)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function parseServerToHistory (list, callback) {
|
||||
$.get(`${serverurl}/history`)
|
||||
.done(data => {
|
||||
if (data.history) {
|
||||
parseToHistory(list, data.history, callback)
|
||||
}
|
||||
})
|
||||
.fail((xhr, status, error) => {
|
||||
console.error(xhr.responseText)
|
||||
})
|
||||
.done(data => {
|
||||
if (data.history) {
|
||||
parseToHistory(list, data.history, callback)
|
||||
}
|
||||
})
|
||||
.fail((xhr, status, error) => {
|
||||
console.error(xhr.responseText)
|
||||
})
|
||||
}
|
||||
|
||||
export function parseStorageToHistory (list, callback) {
|
||||
|
@ -268,15 +267,15 @@ function parseToHistory (list, notehistory, callback) {
|
|||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
// parse time to timestamp and fromNow
|
||||
// parse time to timestamp and fromNow
|
||||
const timestamp = (typeof notehistory[i].time === 'number' ? moment(notehistory[i].time) : moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'))
|
||||
notehistory[i].timestamp = timestamp.valueOf()
|
||||
notehistory[i].fromNow = timestamp.fromNow()
|
||||
notehistory[i].time = timestamp.format('llll')
|
||||
// prevent XSS
|
||||
notehistory[i].text = S(notehistory[i].text).escapeHTML().s
|
||||
notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? S(notehistory[i].tags).escapeHTML().s.split(',') : []
|
||||
// add to list
|
||||
// prevent XSS
|
||||
notehistory[i].text = escapeHTML(notehistory[i].text)
|
||||
notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? escapeHTML(notehistory[i].tags).split(',') : []
|
||||
// add to list
|
||||
if (notehistory[i].id && list.get('id', notehistory[i].id).length === 0) { list.add(notehistory[i]) }
|
||||
}
|
||||
}
|
||||
|
|
1116
public/js/index.js
1116
public/js/index.js
File diff suppressed because it is too large
Load Diff
|
@ -60,22 +60,22 @@ export function checkIfAuth (yesCallback, noCallback) {
|
|||
if (checkLoginStateChanged()) checkAuth = false
|
||||
if (!checkAuth || typeof cookieLoginState === 'undefined') {
|
||||
$.get(`${serverurl}/me`)
|
||||
.done(data => {
|
||||
if (data && data.status === 'ok') {
|
||||
profile = data
|
||||
yesCallback(profile)
|
||||
setLoginState(true, data.id)
|
||||
} else {
|
||||
noCallback()
|
||||
setLoginState(false)
|
||||
}
|
||||
})
|
||||
.fail(() => {
|
||||
noCallback()
|
||||
})
|
||||
.always(() => {
|
||||
checkAuth = true
|
||||
})
|
||||
.done(data => {
|
||||
if (data && data.status === 'ok') {
|
||||
profile = data
|
||||
yesCallback(profile)
|
||||
setLoginState(true, data.id)
|
||||
} else {
|
||||
noCallback()
|
||||
setLoginState(false)
|
||||
}
|
||||
})
|
||||
.fail(() => {
|
||||
noCallback()
|
||||
})
|
||||
.always(() => {
|
||||
checkAuth = true
|
||||
})
|
||||
} else if (cookieLoginState) {
|
||||
yesCallback(profile)
|
||||
} else {
|
||||
|
|
|
@ -51,7 +51,7 @@ export function insertText (cm, text, cursorEnd = 0) {
|
|||
let cursor = cm.getCursor()
|
||||
cm.replaceSelection(text, cursor, cursor)
|
||||
cm.focus()
|
||||
cm.setCursor({line: cursor.line, ch: cursor.ch + cursorEnd})
|
||||
cm.setCursor({ line: cursor.line, ch: cursor.ch + cursorEnd })
|
||||
}
|
||||
|
||||
export function insertLink (cm, isImage) {
|
||||
|
@ -80,7 +80,7 @@ export function insertLink (cm, isImage) {
|
|||
cm.setSelections(ranges)
|
||||
} else {
|
||||
cm.replaceRange(symbol + linkEnd, cursor, cursor)
|
||||
cm.setCursor({line: cursor.line, ch: cursor.ch + symbol.length + linkEnd.length})
|
||||
cm.setCursor({ line: cursor.line, ch: cursor.ch + symbol.length + linkEnd.length })
|
||||
}
|
||||
}
|
||||
cm.focus()
|
||||
|
@ -88,8 +88,8 @@ export function insertLink (cm, isImage) {
|
|||
|
||||
export function insertHeader (cm) {
|
||||
let cursor = cm.getCursor()
|
||||
let startOfLine = {line: cursor.line, ch: 0}
|
||||
let startOfLineText = cm.getRange(startOfLine, {line: cursor.line, ch: 1})
|
||||
let startOfLine = { line: cursor.line, ch: 0 }
|
||||
let startOfLineText = cm.getRange(startOfLine, { line: cursor.line, ch: 1 })
|
||||
// See if it is already a header
|
||||
if (startOfLineText === '#') {
|
||||
cm.replaceRange('#', startOfLine, startOfLine)
|
||||
|
@ -108,14 +108,14 @@ export function insertOnStartOfLines (cm, symbol) {
|
|||
if (!range.empty()) {
|
||||
const from = range.from()
|
||||
const to = range.to()
|
||||
let selection = cm.getRange({line: from.line, ch: 0}, to)
|
||||
let selection = cm.getRange({ line: from.line, ch: 0 }, to)
|
||||
selection = selection.replace(/\n/g, '\n' + symbol)
|
||||
selection = symbol + selection
|
||||
cm.replaceRange(selection, from, to)
|
||||
} else {
|
||||
cm.replaceRange(symbol, {line: cursor.line, ch: 0}, {line: cursor.line, ch: 0})
|
||||
cm.replaceRange(symbol, { line: cursor.line, ch: 0 }, { line: cursor.line, ch: 0 })
|
||||
}
|
||||
}
|
||||
cm.setCursor({line: cursor.line, ch: cursor.ch + symbol.length})
|
||||
cm.setCursor({ line: cursor.line, ch: cursor.ch + symbol.length })
|
||||
cm.focus()
|
||||
}
|
||||
|
|
|
@ -188,7 +188,7 @@ function buildMapInner (callback) {
|
|||
}
|
||||
|
||||
nonEmptyList.push(0)
|
||||
// make the first line go top
|
||||
// make the first line go top
|
||||
_scrollMap[0] = viewTop
|
||||
|
||||
const parts = markdownArea.find('.part').toArray()
|
||||
|
@ -336,7 +336,7 @@ export function syncScrollToView (event, preventAnimate) {
|
|||
const scrollInfo = editor.getScrollInfo()
|
||||
const textHeight = editor.defaultTextHeight()
|
||||
lineNo = Math.floor(scrollInfo.top / textHeight)
|
||||
// if reach the last line, will start lerp to the bottom
|
||||
// if reach the last line, will start lerp to the bottom
|
||||
const diffToBottom = (scrollInfo.top + scrollInfo.clientHeight) - (scrollInfo.height - textHeight)
|
||||
if (scrollInfo.height > scrollInfo.clientHeight && diffToBottom > 0) {
|
||||
topDiffPercent = diffToBottom / textHeight
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
/* eslint-env browser, jquery */
|
||||
/* global refreshView */
|
||||
|
||||
import {
|
||||
autoLinkify,
|
||||
deduplicatedHeaderId,
|
||||
removeDOMEvents,
|
||||
finishView,
|
||||
generateToc,
|
||||
md,
|
||||
parseMeta,
|
||||
postProcess,
|
||||
renderTOC,
|
||||
scrollToHash,
|
||||
smoothHashScroll,
|
||||
updateLastChange
|
||||
} from './extra'
|
||||
|
||||
import { preventXSS } from './render'
|
||||
|
||||
require('../css/extra.css')
|
||||
require('../css/slide-preview.css')
|
||||
require('../css/site.css')
|
||||
|
||||
require('highlight.js/styles/github-gist.css')
|
||||
|
||||
import {
|
||||
autoLinkify,
|
||||
deduplicatedHeaderId,
|
||||
removeDOMEvents,
|
||||
finishView,
|
||||
generateToc,
|
||||
md,
|
||||
parseMeta,
|
||||
postProcess,
|
||||
renderTOC,
|
||||
scrollToHash,
|
||||
smoothHashScroll,
|
||||
updateLastChange
|
||||
} from './extra'
|
||||
|
||||
import { preventXSS } from './render'
|
||||
|
||||
const markdown = $('#doc.markdown-body')
|
||||
const text = markdown.text()
|
||||
const lastMeta = md.meta
|
||||
|
@ -38,7 +38,7 @@ if (md.meta.type && md.meta.type === 'slide') {
|
|||
const slides = window.RevealMarkdown.slidify(text, slideOptions)
|
||||
markdown.html(slides)
|
||||
window.RevealMarkdown.initialize()
|
||||
// prevent XSS
|
||||
// prevent XSS
|
||||
markdown.html(preventXSS(markdown.html()))
|
||||
markdown.addClass('slides')
|
||||
} else {
|
||||
|
@ -46,12 +46,12 @@ if (md.meta.type && md.meta.type === 'slide') {
|
|||
refreshView()
|
||||
markdown.removeClass('slides')
|
||||
}
|
||||
// only render again when meta changed
|
||||
// only render again when meta changed
|
||||
if (JSON.stringify(md.meta) !== JSON.stringify(lastMeta)) {
|
||||
parseMeta(md, null, markdown, $('#ui-toc'), $('#ui-toc-affix'))
|
||||
rendered = md.render(text)
|
||||
}
|
||||
// prevent XSS
|
||||
// prevent XSS
|
||||
rendered = preventXSS(rendered)
|
||||
const result = postProcess(rendered)
|
||||
markdown.html(result.html())
|
||||
|
@ -98,14 +98,14 @@ function generateScrollspy () {
|
|||
}
|
||||
|
||||
function windowResize () {
|
||||
// toc right
|
||||
// toc right
|
||||
const paddingRight = parseFloat(markdown.css('padding-right'))
|
||||
const right = ($(window).width() - (markdown.offset().left + markdown.outerWidth() - paddingRight))
|
||||
toc.css('right', `${right}px`)
|
||||
// affix toc left
|
||||
// affix toc left
|
||||
let newbool
|
||||
const rightMargin = (markdown.parent().outerWidth() - markdown.outerWidth()) / 2
|
||||
// for ipad or wider device
|
||||
// for ipad or wider device
|
||||
if (rightMargin >= 133) {
|
||||
newbool = true
|
||||
const affixLeftMargin = (tocAffix.outerWidth() - tocAffix.width()) / 2
|
||||
|
@ -126,7 +126,7 @@ $(document).ready(() => {
|
|||
windowResize()
|
||||
generateScrollspy()
|
||||
setTimeout(scrollToHash, 0)
|
||||
// tooltip
|
||||
// tooltip
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
})
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ var filterXSSOptions = {
|
|||
onIgnoreTag: function (tag, html, options) {
|
||||
// allow comment tag
|
||||
if (tag === '!--') {
|
||||
// do not filter its attributes
|
||||
// do not filter its attributes
|
||||
return html.replace(/<(?!!--)/g, '<').replace(/-->/g, '__HTML_COMMENT_END__').replace(/>/g, '>').replace(/__HTML_COMMENT_END__/g, '-->')
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
/* eslint-env browser, jquery */
|
||||
/* global serverurl, Reveal, RevealMarkdown */
|
||||
|
||||
require('../css/extra.css')
|
||||
require('../css/site.css')
|
||||
|
||||
import { preventXSS } from './render'
|
||||
import { md, updateLastChange, removeDOMEvents, finishView } from './extra'
|
||||
|
||||
require('../css/extra.css')
|
||||
require('../css/site.css')
|
||||
|
||||
const body = preventXSS($('.slides').text())
|
||||
|
||||
window.createtime = window.lastchangeui.time.attr('data-createtime')
|
||||
|
@ -17,7 +17,7 @@ $('.ui-edit').attr('href', `${url}/edit`)
|
|||
$('.ui-print').attr('href', `${url}?print-pdf`)
|
||||
|
||||
$(document).ready(() => {
|
||||
// tooltip
|
||||
// tooltip
|
||||
$('[data-toggle="tooltip"]').tooltip()
|
||||
})
|
||||
|
||||
|
@ -133,7 +133,7 @@ function renderSlide (event) {
|
|||
Reveal.addEventListener('ready', event => {
|
||||
renderSlide(event)
|
||||
const markdown = $(event.currentSlide)
|
||||
// force browser redraw
|
||||
// force browser redraw
|
||||
setTimeout(() => {
|
||||
markdown.hide().show(0)
|
||||
}, 0)
|
||||
|
|
|
@ -24,13 +24,13 @@
|
|||
</a>
|
||||
<% } %>
|
||||
<% if (authProviders.gitlab) { %>
|
||||
<a href="<%- serverURL %>/auth/gitlab" class="btn btn-lg btn-block btn-social btn-soundcloud">
|
||||
<a href="<%- serverURL %>/auth/gitlab" class="btn btn-lg btn-block btn-social btn-gitlab">
|
||||
<i class="fa fa-gitlab"></i> <%= __('Sign in via %s', 'GitLab') %>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (authProviders.mattermost) { %>
|
||||
<a href="<%- serverURL %>/auth/mattermost" class="btn btn-lg btn-block btn-social btn-soundcloud">
|
||||
<i class="fa fa-mattermost"></i> <%= __('Sign in via %s', 'Mattermost') %>
|
||||
<a href="<%- serverURL %>/auth/mattermost" class="btn btn-lg btn-block btn-social btn-mattermost">
|
||||
<i class="oauth-icon"><img alt="mattermost-logo" src="<%- serverURL %>/images/mattermost-logo.svg" /></i> <%= __('Sign in via %s', 'Mattermost') %>
|
||||
</a>
|
||||
<% } %>
|
||||
<% if (authProviders.dropbox) { %>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
'use strict'
|
||||
|
||||
function stripTags (s) {
|
||||
return s.replace(RegExp(`</?[^<>]*>`, 'gi'), '')
|
||||
}
|
||||
|
||||
exports.stripTags = stripTags
|
|
@ -195,15 +195,11 @@ module.exports = {
|
|||
'bootstrap-validator',
|
||||
'expose-loader?select2!select2',
|
||||
'expose-loader?moment!moment',
|
||||
'script-loader!js-url',
|
||||
path.join(__dirname, 'public/js/cover.js')
|
||||
],
|
||||
index: [
|
||||
'babel-polyfill',
|
||||
'script-loader!jquery-ui-resizable',
|
||||
'script-loader!js-url',
|
||||
'script-loader!Idle.Js',
|
||||
'expose-loader?LZString!lz-string',
|
||||
'script-loader!codemirror',
|
||||
'script-loader!inlineAttachment',
|
||||
'script-loader!jqueryTextcomplete',
|
||||
|
@ -211,23 +207,23 @@ module.exports = {
|
|||
'script-loader!codemirrorInlineAttachment',
|
||||
'script-loader!ot',
|
||||
'flowchart.js',
|
||||
'js-sequence-diagrams',
|
||||
'script-loader!js-sequence-diagrams',
|
||||
'expose-loader?RevealMarkdown!reveal-markdown',
|
||||
path.join(__dirname, 'public/js/index.js')
|
||||
],
|
||||
'index-styles': [
|
||||
path.join(__dirname, 'public/vendor/jquery-ui/jquery-ui.min.css'),
|
||||
path.join(__dirname, 'public/vendor/codemirror-spell-checker/spell-checker.min.css'),
|
||||
path.join(__dirname, 'node_modules/codemirror/lib/codemirror.css'),
|
||||
path.join(__dirname, 'node_modules/codemirror/addon/fold/foldgutter.css'),
|
||||
path.join(__dirname, 'node_modules/codemirror/addon/display/fullscreen.css'),
|
||||
path.join(__dirname, 'node_modules/codemirror/addon/dialog/dialog.css'),
|
||||
path.join(__dirname, 'node_modules/codemirror/addon/scroll/simplescrollbars.css'),
|
||||
path.join(__dirname, 'node_modules/codemirror/addon/search/matchesonscrollbar.css'),
|
||||
path.join(__dirname, 'node_modules/codemirror/theme/monokai.css'),
|
||||
path.join(__dirname, 'node_modules/codemirror/theme/one-dark.css'),
|
||||
path.join(__dirname, 'node_modules/codemirror/mode/tiddlywiki/tiddlywiki.css'),
|
||||
path.join(__dirname, 'node_modules/codemirror/mode/mediawiki/mediawiki.css'),
|
||||
path.join(__dirname, 'node_modules/@hackmd/codemirror/lib/codemirror.css'),
|
||||
path.join(__dirname, 'node_modules/@hackmd/codemirror/addon/fold/foldgutter.css'),
|
||||
path.join(__dirname, 'node_modules/@hackmd/codemirror/addon/display/fullscreen.css'),
|
||||
path.join(__dirname, 'node_modules/@hackmd/codemirror/addon/dialog/dialog.css'),
|
||||
path.join(__dirname, 'node_modules/@hackmd/codemirror/addon/scroll/simplescrollbars.css'),
|
||||
path.join(__dirname, 'node_modules/@hackmd/codemirror/addon/search/matchesonscrollbar.css'),
|
||||
path.join(__dirname, 'node_modules/@hackmd/codemirror/theme/monokai.css'),
|
||||
path.join(__dirname, 'node_modules/@hackmd/codemirror/theme/one-dark.css'),
|
||||
path.join(__dirname, 'node_modules/@hackmd/codemirror/mode/tiddlywiki/tiddlywiki.css'),
|
||||
path.join(__dirname, 'node_modules/@hackmd/codemirror/mode/mediawiki/mediawiki.css'),
|
||||
path.join(__dirname, 'public/css/github-extract.css'),
|
||||
path.join(__dirname, 'public/vendor/showup/showup.css'),
|
||||
path.join(__dirname, 'public/css/mermaid.css'),
|
||||
|
@ -248,13 +244,10 @@ module.exports = {
|
|||
'expose-loader?jsyaml!js-yaml',
|
||||
'script-loader!mermaid',
|
||||
'expose-loader?moment!moment',
|
||||
'script-loader!js-url',
|
||||
'script-loader!handlebars',
|
||||
'expose-loader?hljs!highlight.js',
|
||||
'expose-loader?emojify!emojify.js',
|
||||
'script-loader!Idle.Js',
|
||||
'script-loader!gist-embed',
|
||||
'expose-loader?LZString!lz-string',
|
||||
'script-loader!codemirror',
|
||||
'script-loader!inlineAttachment',
|
||||
'script-loader!jqueryTextcomplete',
|
||||
|
@ -262,7 +255,7 @@ module.exports = {
|
|||
'script-loader!codemirrorInlineAttachment',
|
||||
'script-loader!ot',
|
||||
'flowchart.js',
|
||||
'js-sequence-diagrams',
|
||||
'script-loader!js-sequence-diagrams',
|
||||
'expose-loader?Viz!viz.js',
|
||||
'script-loader!abcjs',
|
||||
'expose-loader?io!socket.io-client',
|
||||
|
@ -272,7 +265,7 @@ module.exports = {
|
|||
pretty: [
|
||||
'babel-polyfill',
|
||||
'flowchart.js',
|
||||
'js-sequence-diagrams',
|
||||
'script-loader!js-sequence-diagrams',
|
||||
'expose-loader?RevealMarkdown!reveal-markdown',
|
||||
path.join(__dirname, 'public/js/pretty.js')
|
||||
],
|
||||
|
@ -297,7 +290,7 @@ module.exports = {
|
|||
'expose-loader?emojify!emojify.js',
|
||||
'script-loader!gist-embed',
|
||||
'flowchart.js',
|
||||
'js-sequence-diagrams',
|
||||
'script-loader!js-sequence-diagrams',
|
||||
'expose-loader?Viz!viz.js',
|
||||
'script-loader!abcjs',
|
||||
'expose-loader?RevealMarkdown!reveal-markdown',
|
||||
|
@ -307,7 +300,7 @@ module.exports = {
|
|||
'babel-polyfill',
|
||||
'bootstrap-tooltip',
|
||||
'flowchart.js',
|
||||
'js-sequence-diagrams',
|
||||
'script-loader!js-sequence-diagrams',
|
||||
'expose-loader?RevealMarkdown!reveal-markdown',
|
||||
path.join(__dirname, 'public/js/slide.js')
|
||||
],
|
||||
|
@ -335,7 +328,7 @@ module.exports = {
|
|||
'expose-loader?emojify!emojify.js',
|
||||
'script-loader!gist-embed',
|
||||
'flowchart.js',
|
||||
'js-sequence-diagrams',
|
||||
'script-loader!js-sequence-diagrams',
|
||||
'expose-loader?Viz!viz.js',
|
||||
'script-loader!abcjs',
|
||||
'headjs',
|
||||
|
@ -355,7 +348,7 @@ module.exports = {
|
|||
modules: ['node_modules'],
|
||||
extensions: ['.js'],
|
||||
alias: {
|
||||
codemirror: path.join(__dirname, 'node_modules/codemirror/codemirror.min.js'),
|
||||
codemirror: path.join(__dirname, 'node_modules/@hackmd/codemirror/codemirror.min.js'),
|
||||
inlineAttachment: path.join(__dirname, 'public/vendor/inlineAttachment/inline-attachment.js'),
|
||||
jqueryTextcomplete: path.join(__dirname, 'public/vendor/jquery-textcomplete/jquery.textcomplete.js'),
|
||||
codemirrorSpellChecker: path.join(__dirname, 'public/vendor/codemirror-spell-checker/spell-checker.min.js'),
|
||||
|
@ -369,7 +362,8 @@ module.exports = {
|
|||
'headjs': path.join(__dirname, 'node_modules/reveal.js/lib/js/head.min.js'),
|
||||
'reveal-markdown': path.join(__dirname, 'public/js/reveal-markdown.js'),
|
||||
abcjs: path.join(__dirname, 'public/vendor/abcjs_basic_3.1.1-min.js'),
|
||||
raphael: path.join(__dirname, 'node_modules/raphael/raphael.no-deps.js')
|
||||
raphael: path.join(__dirname, 'node_modules/raphael/raphael.min.js'),
|
||||
'js-sequence-diagrams': path.join(__dirname, 'node_modules/@hackmd/js-sequence-diagrams/build/main.js')
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -418,12 +412,6 @@ module.exports = {
|
|||
},
|
||||
'less-loader'
|
||||
]
|
||||
}, {
|
||||
test: require.resolve('js-sequence-diagrams'),
|
||||
use: [{
|
||||
loader: 'imports-loader',
|
||||
options: { _: 'lodash', Raphael: 'raphael', eve: 'eve' }
|
||||
}]
|
||||
}, {
|
||||
test: /\.eot(\?v=\d+\.\d+\.\d+)?$/,
|
||||
use: [{ loader: 'file-loader' }]
|
||||
|
|
Loading…
Reference in New Issue