Merge branch 'master' into toc
This commit is contained in:
20
.eslintrc
20
.eslintrc
@@ -6,10 +6,7 @@
|
||||
"plugin:import/warnings",
|
||||
"plugin:flowtype/recommended"
|
||||
],
|
||||
"plugins": [
|
||||
"prettier",
|
||||
"flowtype"
|
||||
],
|
||||
"plugins": ["prettier", "flowtype"],
|
||||
"rules": {
|
||||
"eqeqeq": 2,
|
||||
"no-unused-vars": 2,
|
||||
@@ -22,7 +19,7 @@
|
||||
"import/no-unresolved": [
|
||||
"error",
|
||||
{
|
||||
"ignore": [ "slate-drop-or-paste-images" ]
|
||||
"ignore": ["slate-drop-or-paste-images"]
|
||||
}
|
||||
],
|
||||
// Flow
|
||||
@@ -33,14 +30,8 @@
|
||||
"annotationStyle": "line"
|
||||
}
|
||||
],
|
||||
"flowtype/space-after-type-colon": [
|
||||
2,
|
||||
"always"
|
||||
],
|
||||
"flowtype/space-before-type-colon": [
|
||||
2,
|
||||
"never"
|
||||
],
|
||||
"flowtype/space-after-type-colon": [2, "always"],
|
||||
"flowtype/space-before-type-colon": [2, "never"],
|
||||
// Enforce that code is formatted with prettier.
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
@@ -65,7 +56,8 @@
|
||||
"SLACK_REDIRECT_URI": true,
|
||||
"DEPLOYMENT": true,
|
||||
"BASE_URL": true,
|
||||
"BUGSNAG_KEY": true,
|
||||
"afterAll": true,
|
||||
"Bugsnag": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
1. Install dependencies with `yarn`
|
||||
1. Register a Slack app at https://api.slack.com/apps
|
||||
1. Copy the file `.env.sample` to `.env` and fill out the keys
|
||||
1. Run DB migrations `yarn run sequelize -- db:migrate`
|
||||
1. Run DB migrations `yarn sequelize -- db:migrate`
|
||||
1. Start the development server `yarn start`
|
||||
|
||||
|
||||
@@ -16,12 +16,12 @@
|
||||
Sequelize is used to create and run migrations, for example:
|
||||
|
||||
```
|
||||
yarn run sequelize migration:create
|
||||
yarn run sequelize db:migrate
|
||||
yarn sequelize migration:create
|
||||
yarn sequelize db:migrate
|
||||
```
|
||||
|
||||
Or to run migrations on test database:
|
||||
|
||||
```
|
||||
yarn run sequelize db:migrate -- --env test
|
||||
yarn sequelize db:migrate --env test
|
||||
```
|
||||
|
||||
3
app.json
3
app.json
@@ -45,6 +45,9 @@
|
||||
},
|
||||
"URL": {
|
||||
"required": true
|
||||
},
|
||||
"BUGSNAG_KEY": {
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"formation": {},
|
||||
|
||||
1
flow-typed/globals.js
vendored
1
flow-typed/globals.js
vendored
@@ -3,5 +3,6 @@ declare var __DEV__: string;
|
||||
declare var SLACK_REDIRECT_URI: string;
|
||||
declare var SLACK_KEY: string;
|
||||
declare var BASE_URL: string;
|
||||
declare var BUGSNAG_KEY: ?string;
|
||||
declare var DEPLOYMENT: string;
|
||||
declare var Bugsnag: any;
|
||||
|
||||
@@ -24,7 +24,7 @@ type Props = {
|
||||
onCancel: Function,
|
||||
onImageUploadStart: Function,
|
||||
onImageUploadStop: Function,
|
||||
emoji: string,
|
||||
emoji?: string,
|
||||
readOnly: boolean,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
|
||||
const HtmlContent = styled.div`
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
:global {
|
||||
.anchor {
|
||||
visibility: hidden;
|
||||
color: ;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
:global {
|
||||
.anchor {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 1.5em;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-style: italic;
|
||||
border-left: 2px solid $lightGray;
|
||||
padding-left: 0.8em;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
display: block;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead, tbody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
thead {
|
||||
tr {
|
||||
border-bottom: 2px solid $lightGray;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border-bottom: 1px solid $lightGray;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: #fff;
|
||||
|
||||
// &:nth-child(2n) {
|
||||
// background-color: #f8f8f8;
|
||||
// }
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
border: 1px 0 solid $lightGray;
|
||||
padding: 5px 20px 5px 0;
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default HtmlContent;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import HtmlContent from './HtmlContent';
|
||||
export default HtmlContent;
|
||||
@@ -16,7 +16,7 @@ const activeStyle = {
|
||||
const StyleableDiv = props => <div {...props} />;
|
||||
|
||||
const styleComponent = component => styled(component)`
|
||||
display: block;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -42,7 +42,7 @@ function SidebarLink(props: Object) {
|
||||
<Flex>
|
||||
<Component exact activeStyle={activeStyle} {...props}>
|
||||
{props.hasChildren && <StyledChevron expanded={props.expanded} />}
|
||||
{props.children}
|
||||
<Content>{props.children}</Content>
|
||||
</Component>
|
||||
</Flex>
|
||||
);
|
||||
@@ -62,4 +62,8 @@ const StyledChevron = styled(ChevronIcon)`
|
||||
}
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export default SidebarLink;
|
||||
|
||||
@@ -73,6 +73,15 @@ const Auth = ({ children }: AuthProps) => {
|
||||
}),
|
||||
};
|
||||
|
||||
if (window.Bugsnag) {
|
||||
Bugsnag.user = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
teamId: team.id,
|
||||
team: team.name,
|
||||
};
|
||||
}
|
||||
|
||||
authenticatedStores.collections.fetchAll();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,30 +2,31 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import HtmlContent from 'components/HtmlContent';
|
||||
import Editor from 'components/Editor';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
|
||||
import { convertToMarkdown } from 'utils/markdown';
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
content: string,
|
||||
};
|
||||
|
||||
@observer class Flatpage extends React.Component {
|
||||
props: Props;
|
||||
const Flatpage = observer((props: Props) => {
|
||||
const { title, content } = props;
|
||||
|
||||
render() {
|
||||
const { title, content } = this.props;
|
||||
const htmlContent = convertToMarkdown(content);
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title={title} />
|
||||
<HtmlContent dangerouslySetInnerHTML={{ __html: htmlContent }} />
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title={title} />
|
||||
<Editor
|
||||
text={content}
|
||||
onChange={() => {}}
|
||||
onSave={() => {}}
|
||||
onCancel={() => {}}
|
||||
onImageUploadStart={() => {}}
|
||||
onImageUploadStop={() => {}}
|
||||
readOnly
|
||||
/>
|
||||
</CenteredContent>
|
||||
);
|
||||
});
|
||||
|
||||
export default Flatpage;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,16 +0,0 @@
|
||||
// @flow
|
||||
import emojiMapping from './emoji-mapping.json';
|
||||
|
||||
const EMOJI_REGEX = /:([A-Za-z0-9_\-+]+?):/gm;
|
||||
|
||||
const emojify = (text: string = '') => {
|
||||
let emojifiedText = text;
|
||||
|
||||
emojifiedText = text.replace(EMOJI_REGEX, (match, p1, offset, string) => {
|
||||
return emojiMapping[p1] || match;
|
||||
});
|
||||
|
||||
return emojifiedText;
|
||||
};
|
||||
|
||||
export default emojify;
|
||||
@@ -1,53 +0,0 @@
|
||||
// @flow
|
||||
import slug from 'slug';
|
||||
import marked from 'marked';
|
||||
import sanitizedRenderer from 'marked-sanitized';
|
||||
import highlight from 'highlight.js';
|
||||
import _ from 'lodash';
|
||||
import emojify from './emojify';
|
||||
import toc from './toc';
|
||||
|
||||
// $FlowIssue invalid flow-typed
|
||||
slug.defaults.mode = 'rfc3986';
|
||||
|
||||
const Renderer = sanitizedRenderer(marked.Renderer);
|
||||
const renderer = new Renderer();
|
||||
renderer.code = (code, language) => {
|
||||
const validLang = !!(language && highlight.getLanguage(language));
|
||||
const highlighted = validLang
|
||||
? highlight.highlight(language, code).value
|
||||
: _.escape(code);
|
||||
return `<pre><code class="hljs ${_.escape(language)}">${highlighted}</code></pre>`;
|
||||
};
|
||||
renderer.heading = (text, level) => {
|
||||
const headingSlug = _.escape(slug(text));
|
||||
return `
|
||||
<h${level}>
|
||||
${text}
|
||||
<a name="${headingSlug}" class="anchor" href="#${headingSlug}">#</a>
|
||||
</h${level}>
|
||||
`;
|
||||
};
|
||||
|
||||
const convertToMarkdown = (text: string) => {
|
||||
// Add TOC
|
||||
text = toc.insert(text || '', {
|
||||
slugify: heading => {
|
||||
// FIXME: E.g. `&` gets messed up
|
||||
const headingSlug = _.escape(slug(heading));
|
||||
return headingSlug;
|
||||
},
|
||||
});
|
||||
|
||||
return marked.parse(emojify(text), {
|
||||
renderer,
|
||||
gfm: true,
|
||||
tables: true,
|
||||
breaks: false,
|
||||
pedantic: false,
|
||||
smartLists: true,
|
||||
smartypants: true,
|
||||
});
|
||||
};
|
||||
|
||||
export { convertToMarkdown };
|
||||
@@ -1,148 +0,0 @@
|
||||
// @flow
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* marked-toc <https://github.com/jonschlinkert/marked-toc>
|
||||
*
|
||||
* Copyright (c) 2014 Jon Schlinkert, contributors.
|
||||
* Licensed under the MIT license.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var marked = require('marked');
|
||||
var _ = require('lodash');
|
||||
var utils = require('./utils');
|
||||
|
||||
/**
|
||||
* Expose `toc`
|
||||
*/
|
||||
|
||||
module.exports = toc;
|
||||
|
||||
/**
|
||||
* Default template to use for generating
|
||||
* a table of contents.
|
||||
*/
|
||||
|
||||
var defaultTemplate =
|
||||
'<%= depth %><%= bullet %>[<%= heading %>](#<%= url %>)\n';
|
||||
|
||||
/**
|
||||
* Create the table of contents object that
|
||||
* will be used as context for the template.
|
||||
*
|
||||
* @param {String} `str`
|
||||
* @param {Object} `options`
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function generate(str, options) {
|
||||
var opts = _.extend(
|
||||
{
|
||||
firsth1: false,
|
||||
blacklist: true,
|
||||
omit: [],
|
||||
maxDepth: 3,
|
||||
slugify: function(text) {
|
||||
return text; // Override this!
|
||||
},
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
var toc = '';
|
||||
// $FlowIssue invalid flow-typed
|
||||
var tokens = marked.lexer(str);
|
||||
var tocArray = [];
|
||||
|
||||
// Remove the very first h1, true by default
|
||||
if (opts.firsth1 === false) {
|
||||
tokens.shift();
|
||||
}
|
||||
|
||||
// Do any h1's still exist?
|
||||
var h1 = _.some(tokens, { depth: 1 });
|
||||
|
||||
tokens
|
||||
.filter(function(token) {
|
||||
// Filter out everything but headings
|
||||
if (token.type !== 'heading' || token.type === 'code') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Since we removed the first h1, we'll check to see if other h1's
|
||||
// exist. If none exist, then we unindent the rest of the TOC
|
||||
if (!h1) {
|
||||
token.depth = token.depth - 1;
|
||||
}
|
||||
|
||||
// Store original text and create an id for linking
|
||||
token.heading = opts.strip ? utils.strip(token.text, opts) : token.text;
|
||||
|
||||
// Create a "slugified" id for linking
|
||||
token.id = opts.slugify(token.text);
|
||||
|
||||
// Omit headings with these strings
|
||||
var omissions = ['Table of Contents', 'TOC', 'TABLE OF CONTENTS'];
|
||||
var omit = _.union([], opts.omit, omissions);
|
||||
|
||||
if (utils.isMatch(omit, token.heading)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.forEach(function(h) {
|
||||
if (h.depth > opts.maxDepth) {
|
||||
return;
|
||||
}
|
||||
|
||||
var bullet = Array.isArray(opts.bullet)
|
||||
? opts.bullet[(h.depth - 1) % opts.bullet.length]
|
||||
: opts.bullet;
|
||||
|
||||
var data = _.extend({}, opts.data, {
|
||||
depth: new Array((h.depth - 1) * 2 + 1).join(' '),
|
||||
bullet: bullet ? bullet : '* ',
|
||||
heading: h.heading,
|
||||
url: h.id,
|
||||
});
|
||||
|
||||
tocArray.push(data);
|
||||
toc += _.template(opts.template || defaultTemplate)(data);
|
||||
});
|
||||
|
||||
return {
|
||||
data: tocArray,
|
||||
toc: opts.strip ? utils.strip(toc, opts) : toc,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* toc
|
||||
*/
|
||||
|
||||
function toc(str: string, options: Object) {
|
||||
return generate(str, options).toc;
|
||||
}
|
||||
|
||||
toc.raw = function(str, options) {
|
||||
return generate(str, options);
|
||||
};
|
||||
|
||||
toc.insert = function(content, options) {
|
||||
var start = '<!-- toc -->';
|
||||
var stop = '<!-- tocstop -->';
|
||||
var re = /<!-- toc -->([\s\S]+?)<!-- tocstop -->/;
|
||||
|
||||
// remove the existing TOC
|
||||
content = content.replace(re, start);
|
||||
|
||||
// generate new TOC
|
||||
var newtoc =
|
||||
'\n\n' + start + '\n\n' + toc(content, options) + '\n' + stop + '\n';
|
||||
|
||||
// If front-matter existed, put it back
|
||||
return content.replace(start, newtoc);
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
/*!
|
||||
* marked-toc <https://github.com/jonschlinkert/marked-toc>
|
||||
*
|
||||
* Copyright (c) 2014 Jon Schlinkert, contributors.
|
||||
* Licensed under the MIT license.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var _ = require('lodash');
|
||||
var utils = (module.exports = {});
|
||||
|
||||
utils.arrayify = function(arr) {
|
||||
return !Array.isArray(arr) ? [arr] : arr;
|
||||
};
|
||||
|
||||
utils.escapeRegex = function(re) {
|
||||
return re.replace(/(\[|\]|\(|\)|\/|\.|\^|\$|\*|\+|\?)/g, '\\$1');
|
||||
};
|
||||
|
||||
utils.isDest = function(dest) {
|
||||
return !dest || dest === 'undefined' || typeof dest === 'object';
|
||||
};
|
||||
|
||||
utils.isMatch = function(keys, str) {
|
||||
keys = utils.arrayify(keys);
|
||||
keys = keys.length > 0 ? keys.join('|') : '.*';
|
||||
|
||||
// Escape certain characters, like '[', '('
|
||||
var k = utils.escapeRegex(String(keys));
|
||||
|
||||
// Build up the regex to use for replacement patterns
|
||||
var re = new RegExp('(?:' + k + ')', 'g');
|
||||
if (String(str).match(re)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
utils.sanitize = function(src) {
|
||||
src = src.replace(/(\s*\[!|(?:\[.+ →\]\()).+/g, '');
|
||||
src = src.replace(/\s*\*\s*\[\].+/g, '');
|
||||
return src;
|
||||
};
|
||||
|
||||
utils.slugify = function(str) {
|
||||
str = str.replace(/\/\//g, '-');
|
||||
str = str.replace(/\//g, '-');
|
||||
str = str.replace(/\./g, '-');
|
||||
str = _.str.slugify(str);
|
||||
str = str.replace(/^-/, '');
|
||||
str = str.replace(/-$/, '');
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Strip certain words from headings. These can be
|
||||
* overridden. Might seem strange but it makes
|
||||
* sense in context.
|
||||
*/
|
||||
|
||||
var omit = [
|
||||
'grunt',
|
||||
'helper',
|
||||
'handlebars-helper',
|
||||
'mixin',
|
||||
'filter',
|
||||
'assemble-contrib',
|
||||
'assemble',
|
||||
];
|
||||
|
||||
utils.strip = function(name, options) {
|
||||
var opts = _.extend({}, options);
|
||||
if (opts.omit === false) {
|
||||
omit = [];
|
||||
}
|
||||
var exclusions = _.union(omit, utils.arrayify(opts.strip || []));
|
||||
var re = new RegExp('^(?:' + exclusions.join('|') + ')[-_]?', 'g');
|
||||
return name.replace(re, '');
|
||||
};
|
||||
@@ -85,7 +85,6 @@
|
||||
"css-loader": "^0.28.7",
|
||||
"debug": "2.2.0",
|
||||
"dotenv": "^4.0.0",
|
||||
"emoji-name-map": "1.1.2",
|
||||
"emoji-regex": "^6.5.1",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-config-react-app": "^0.6.2",
|
||||
@@ -100,7 +99,6 @@
|
||||
"fbemitter": "^2.1.1",
|
||||
"file-loader": "0.9.0",
|
||||
"flow-typed": "^2.1.2",
|
||||
"highlight.js": "9.4.0",
|
||||
"history": "3.0.0",
|
||||
"html-webpack-plugin": "2.17.0",
|
||||
"http-errors": "1.4.0",
|
||||
@@ -108,7 +106,6 @@
|
||||
"invariant": "^2.2.2",
|
||||
"isomorphic-fetch": "2.2.1",
|
||||
"js-search": "^1.4.2",
|
||||
"js-tree": "1.1.0",
|
||||
"json-loader": "0.5.4",
|
||||
"jsonwebtoken": "7.0.1",
|
||||
"koa": "^2.2.0",
|
||||
@@ -125,8 +122,6 @@
|
||||
"localforage": "^1.5.0",
|
||||
"lodash": "^4.17.4",
|
||||
"lodash.orderby": "4.4.0",
|
||||
"marked": "0.3.6",
|
||||
"marked-sanitized": "^0.1.1",
|
||||
"mobx": "^3.1.9",
|
||||
"mobx-react": "^4.1.8",
|
||||
"mobx-react-devtools": "^4.2.11",
|
||||
@@ -170,7 +165,6 @@
|
||||
"string-hash": "^1.1.0",
|
||||
"style-loader": "^0.18.2",
|
||||
"styled-components": "^2.0.0",
|
||||
"truncate-html": "https://github.com/jorilallo/truncate-html/tarball/master",
|
||||
"url-loader": "0.5.7",
|
||||
"uuid": "2.0.2",
|
||||
"validator": "5.2.0",
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var mapping = require('emoji-name-map');
|
||||
|
||||
fs.writeFile(
|
||||
path.join(__dirname, '../frontend/utils/emoji-mapping.json'),
|
||||
JSON.stringify(mapping.emoji)
|
||||
);
|
||||
@@ -4,10 +4,16 @@ import httpErrors from 'http-errors';
|
||||
|
||||
import auth from './middlewares/authentication';
|
||||
import pagination from './middlewares/pagination';
|
||||
import { presentDocument } from '../presenters';
|
||||
import { Document, Collection, Star, View } from '../models';
|
||||
import { presentDocument, presentRevision } from '../presenters';
|
||||
import { Document, Collection, Star, View, Revision } from '../models';
|
||||
|
||||
const authDocumentForUser = (ctx, document) => {
|
||||
const user = ctx.state.user;
|
||||
if (!document || document.teamId !== user.teamId) throw httpErrors.NotFound();
|
||||
};
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post('documents.list', auth(), pagination(), async ctx => {
|
||||
let { sort = 'updatedAt', direction } = ctx.body;
|
||||
if (direction !== 'ASC') direction = 'DESC';
|
||||
@@ -101,23 +107,38 @@ router.post('documents.info', auth(), async ctx => {
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
const document = await Document.findById(id);
|
||||
|
||||
if (!document) throw httpErrors.NotFound();
|
||||
|
||||
// Don't expose private documents outside the team
|
||||
if (document.private) {
|
||||
if (!ctx.state.user) throw httpErrors.NotFound();
|
||||
|
||||
const user = await ctx.state.user;
|
||||
if (document.teamId !== user.teamId) {
|
||||
throw httpErrors.NotFound();
|
||||
}
|
||||
}
|
||||
authDocumentForUser(ctx, document);
|
||||
|
||||
ctx.body = {
|
||||
data: await presentDocument(ctx, document),
|
||||
};
|
||||
});
|
||||
|
||||
router.post('documents.revisions', auth(), pagination(), async ctx => {
|
||||
let { id, sort = 'updatedAt', direction } = ctx.body;
|
||||
if (direction !== 'ASC') direction = 'DESC';
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
const document = await Document.findById(id);
|
||||
|
||||
authDocumentForUser(ctx, document);
|
||||
|
||||
const revisions = await Revision.findAll({
|
||||
where: { documentId: id },
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
|
||||
const data = await Promise.all(
|
||||
revisions.map(revision => presentRevision(ctx, revision))
|
||||
);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
};
|
||||
});
|
||||
|
||||
router.post('documents.search', auth(), async ctx => {
|
||||
const { query } = ctx.body;
|
||||
ctx.assertPresent(query, 'query is required');
|
||||
@@ -142,8 +163,7 @@ router.post('documents.star', auth(), async ctx => {
|
||||
const user = await ctx.state.user;
|
||||
const document = await Document.findById(id);
|
||||
|
||||
if (!document || document.teamId !== user.teamId)
|
||||
throw httpErrors.BadRequest();
|
||||
authDocumentForUser(ctx, document);
|
||||
|
||||
await Star.findOrCreate({
|
||||
where: { documentId: document.id, userId: user.id },
|
||||
@@ -156,8 +176,7 @@ router.post('documents.unstar', auth(), async ctx => {
|
||||
const user = await ctx.state.user;
|
||||
const document = await Document.findById(id);
|
||||
|
||||
if (!document || document.teamId !== user.teamId)
|
||||
throw httpErrors.BadRequest();
|
||||
authDocumentForUser(ctx, document);
|
||||
|
||||
await Star.destroy({
|
||||
where: { documentId: document.id, userId: user.id },
|
||||
@@ -228,7 +247,7 @@ router.post('documents.update', auth(), async ctx => {
|
||||
const document = await Document.findById(id);
|
||||
const collection = document.collection;
|
||||
|
||||
if (!document || document.teamId !== user.teamId) throw httpErrors.NotFound();
|
||||
authDocumentForUser(ctx, document);
|
||||
|
||||
// Update document
|
||||
if (title) document.title = title;
|
||||
@@ -254,15 +273,14 @@ router.post('documents.move', auth(), async ctx => {
|
||||
ctx.assertUuid(parentDocument, 'parentDocument must be an uuid');
|
||||
if (index) ctx.assertPositiveInteger(index, 'index must be an integer (>=0)');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findById(id);
|
||||
const collection = await Collection.findById(document.atlasId);
|
||||
|
||||
authDocumentForUser(ctx, document);
|
||||
|
||||
if (collection.type !== 'atlas')
|
||||
throw httpErrors.BadRequest("This document can't be moved");
|
||||
|
||||
if (!document || document.teamId !== user.teamId) throw httpErrors.NotFound();
|
||||
|
||||
// Set parent document
|
||||
if (parentDocument) {
|
||||
const parent = await Document.findById(parentDocument);
|
||||
@@ -292,12 +310,10 @@ router.post('documents.delete', auth(), async ctx => {
|
||||
const { id } = ctx.body;
|
||||
ctx.assertPresent(id, 'id is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findById(id);
|
||||
const collection = await Collection.findById(document.atlasId);
|
||||
|
||||
if (!document || document.teamId !== user.teamId)
|
||||
throw httpErrors.BadRequest();
|
||||
authDocumentForUser(ctx, document);
|
||||
|
||||
if (collection.type === 'atlas') {
|
||||
// Don't allow deletion of root docs
|
||||
|
||||
@@ -43,6 +43,24 @@ describe('#documents.list', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#documents.revision', async () => {
|
||||
it("should return document's revisions", async () => {
|
||||
const { user, document } = await seed();
|
||||
const res = await server.post('/api/documents.revisions', {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).not.toEqual(document.id);
|
||||
expect(body.data[0].title).toEqual(document.title);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#documents.search', async () => {
|
||||
it('should return results', async () => {
|
||||
const { user } = await seed();
|
||||
|
||||
@@ -65,8 +65,8 @@ if (process.env.NODE_ENV === 'development') {
|
||||
app.use(logger());
|
||||
}
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
bugsnag.register('ad7a85f99b1b9324a31e16732cdf3192');
|
||||
if (process.env.NODE_ENV === 'production' && process.env.BUGSNAG_KEY) {
|
||||
bugsnag.register(process.env.BUGSNAG_KEY);
|
||||
app.on('error', bugsnag.koaHandler);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
up: function(queryInterface, Sequelize) {
|
||||
queryInterface.removeColumn('collections', 'navigationTree');
|
||||
},
|
||||
|
||||
down: function(queryInterface, Sequelize) {
|
||||
queryInterface.addColumn('collections', 'navigationTree', {
|
||||
type: Sequelize.JSONB,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
};
|
||||
23
server/migrations/20171017055026-remove-document-html.js
Normal file
23
server/migrations/20171017055026-remove-document-html.js
Normal file
@@ -0,0 +1,23 @@
|
||||
module.exports = {
|
||||
up: function(queryInterface, Sequelize) {
|
||||
queryInterface.removeColumn('documents', 'html');
|
||||
queryInterface.removeColumn('documents', 'preview');
|
||||
queryInterface.removeColumn('revisions', 'html');
|
||||
queryInterface.removeColumn('revisions', 'preview');
|
||||
},
|
||||
|
||||
down: function(queryInterface, Sequelize) {
|
||||
queryInterface.addColumn('documents', 'html', {
|
||||
type: Sequelize.TEXT,
|
||||
});
|
||||
queryInterface.addColumn('documents', 'preview', {
|
||||
type: Sequelize.TEXT,
|
||||
});
|
||||
queryInterface.addColumn('revisions', 'html', {
|
||||
type: Sequelize.TEXT,
|
||||
});
|
||||
queryInterface.addColumn('revisions', 'preview', {
|
||||
type: Sequelize.TEXT,
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -29,7 +29,6 @@ const Collection = sequelize.define(
|
||||
creatorId: DataTypes.UUID,
|
||||
|
||||
/* type: atlas */
|
||||
navigationTree: DataTypes.JSONB, // legacy
|
||||
documentStructure: DataTypes.JSONB,
|
||||
},
|
||||
{
|
||||
@@ -98,28 +97,6 @@ Collection.prototype.getUrl = function() {
|
||||
return `/collections/${this.id}`;
|
||||
};
|
||||
|
||||
Collection.prototype.getDocumentsStructure = async function() {
|
||||
// Lazy fill this.documentStructure - TMP for internal release
|
||||
if (!this.documentStructure) {
|
||||
this.documentStructure = this.navigationTree.children;
|
||||
|
||||
// Remove parent references from all root documents
|
||||
await this.navigationTree.children.forEach(async ({ id }) => {
|
||||
const document = await Document.findById(id);
|
||||
document.parentDocumentId = null;
|
||||
await document.save();
|
||||
});
|
||||
|
||||
// Remove root document
|
||||
const rootDocument = await Document.findById(this.navigationTree.id);
|
||||
await rootDocument.destroy();
|
||||
|
||||
await this.save();
|
||||
}
|
||||
|
||||
return this.documentStructure;
|
||||
};
|
||||
|
||||
Collection.prototype.addDocumentToStructure = async function(
|
||||
document,
|
||||
index,
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
import slug from 'slug';
|
||||
import _ from 'lodash';
|
||||
import randomstring from 'randomstring';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
|
||||
import isUUID from 'validator/lib/isUUID';
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import { convertToMarkdown } from '../../frontend/utils/markdown';
|
||||
import { truncateMarkdown } from '../utils/truncate';
|
||||
import parseTitle from '../../shared/parseTitle';
|
||||
import Revision from './Revision';
|
||||
|
||||
@@ -25,8 +22,6 @@ const createRevision = doc => {
|
||||
return Revision.create({
|
||||
title: doc.title,
|
||||
text: doc.text,
|
||||
html: doc.html,
|
||||
preview: doc.preview,
|
||||
userId: doc.lastModifiedById,
|
||||
documentId: doc.id,
|
||||
});
|
||||
@@ -40,8 +35,6 @@ const beforeSave = async doc => {
|
||||
const { emoji } = parseTitle(doc.text);
|
||||
|
||||
doc.emoji = emoji;
|
||||
doc.html = convertToMarkdown(doc.text);
|
||||
doc.preview = truncateMarkdown(doc.text, 160);
|
||||
doc.revisionCount += 1;
|
||||
|
||||
// Collaborators
|
||||
@@ -74,8 +67,6 @@ const Document = sequelize.define(
|
||||
private: { type: DataTypes.BOOLEAN, defaultValue: true },
|
||||
title: DataTypes.STRING,
|
||||
text: DataTypes.TEXT,
|
||||
html: DataTypes.TEXT,
|
||||
preview: DataTypes.TEXT,
|
||||
revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 },
|
||||
parentDocumentId: DataTypes.UUID,
|
||||
createdById: {
|
||||
|
||||
@@ -9,8 +9,6 @@ const Revision = sequelize.define('revision', {
|
||||
},
|
||||
title: DataTypes.STRING,
|
||||
text: DataTypes.TEXT,
|
||||
html: DataTypes.TEXT,
|
||||
preview: DataTypes.TEXT,
|
||||
|
||||
userId: {
|
||||
type: 'UUID',
|
||||
|
||||
@@ -19,7 +19,7 @@ async function present(ctx: Object, collection: Collection) {
|
||||
};
|
||||
|
||||
if (collection.type === 'atlas') {
|
||||
data.documents = await collection.getDocumentsStructure();
|
||||
data.documents = collection.documentStructure;
|
||||
}
|
||||
|
||||
if (collection.documents) {
|
||||
|
||||
@@ -21,8 +21,6 @@ async function present(ctx: Object, document: Document, options: ?Options) {
|
||||
private: document.private,
|
||||
title: document.title,
|
||||
text: document.text,
|
||||
html: document.html,
|
||||
preview: document.preview,
|
||||
emoji: document.emoji,
|
||||
createdAt: document.createdAt,
|
||||
createdBy: presentUser(ctx, document.createdBy),
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import presentUser from './user';
|
||||
import presentView from './view';
|
||||
import presentDocument from './document';
|
||||
import presentRevision from './revision';
|
||||
import presentCollection from './collection';
|
||||
import presentApiKey from './apiKey';
|
||||
import presentTeam from './team';
|
||||
@@ -10,6 +11,7 @@ export {
|
||||
presentUser,
|
||||
presentView,
|
||||
presentDocument,
|
||||
presentRevision,
|
||||
presentCollection,
|
||||
presentApiKey,
|
||||
presentTeam,
|
||||
|
||||
15
server/presenters/revision.js
Normal file
15
server/presenters/revision.js
Normal file
@@ -0,0 +1,15 @@
|
||||
// @flow
|
||||
import _ from 'lodash';
|
||||
import { Revision } from '../models';
|
||||
|
||||
function present(ctx: Object, revision: Revision) {
|
||||
return {
|
||||
id: revision.id,
|
||||
title: revision.title,
|
||||
text: revision.text,
|
||||
createdAt: revision.createdAt,
|
||||
updatedAt: revision.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export default present;
|
||||
@@ -26,6 +26,6 @@
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
<script src="//d2wy8f7a9ursnm.cloudfront.net/bugsnag-3.min.js" data-apikey="8165e2069605bc20ccd0792dbbfae7bf"></script>
|
||||
<script src="//d2wy8f7a9ursnm.cloudfront.net/bugsnag-3.min.js" data-apikey="<%= BUGSNAG_KEY %>"></script>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,16 +0,0 @@
|
||||
import truncate from 'truncate-html';
|
||||
import { convertToMarkdown } from '../../frontend/utils/markdown';
|
||||
|
||||
truncate.defaultOptions = {
|
||||
stripTags: false,
|
||||
ellipsis: '...',
|
||||
decodeEntities: false,
|
||||
excludes: ['h1', 'pre'],
|
||||
};
|
||||
|
||||
const truncateMarkdown = (text, length) => {
|
||||
const html = convertToMarkdown(text);
|
||||
return truncate(html, length);
|
||||
};
|
||||
|
||||
export { truncateMarkdown };
|
||||
@@ -13,6 +13,7 @@ const definePlugin = new webpack.DefinePlugin({
|
||||
SLACK_REDIRECT_URI: JSON.stringify(process.env.SLACK_REDIRECT_URI),
|
||||
SLACK_KEY: JSON.stringify(process.env.SLACK_KEY),
|
||||
BASE_URL: JSON.stringify(process.env.URL),
|
||||
BUGSNAG_KEY: JSON.stringify(process.env.BUGSNAG_KEY),
|
||||
DEPLOYMENT: JSON.stringify(process.env.DEPLOYMENT || 'hosted'),
|
||||
});
|
||||
|
||||
|
||||
64
yarn.lock
64
yarn.lock
@@ -1454,7 +1454,7 @@ charenc@~0.0.1:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667"
|
||||
|
||||
cheerio@0.22.0, cheerio@^0.22.0:
|
||||
cheerio@^0.22.0:
|
||||
version "0.22.0"
|
||||
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e"
|
||||
dependencies:
|
||||
@@ -2496,22 +2496,10 @@ elliptic@^6.0.0:
|
||||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.0"
|
||||
|
||||
emoji-name-map@1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/emoji-name-map/-/emoji-name-map-1.1.2.tgz#662f6b5582b0eaf817be6a9ac272fbd8af10ae73"
|
||||
dependencies:
|
||||
emojilib "^2.0.2"
|
||||
iterate-object "^1.3.1"
|
||||
map-o "^2.0.1"
|
||||
|
||||
emoji-regex@^6.1.0, emoji-regex@^6.5.1:
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2"
|
||||
|
||||
emojilib@^2.0.2:
|
||||
version "2.2.9"
|
||||
resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.2.9.tgz#ec5722689fc148f56422c14b0dc16a901d446b75"
|
||||
|
||||
emojis-list@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
|
||||
@@ -3861,10 +3849,6 @@ hide-powered-by@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.0.0.tgz#4a85ad65881f62857fc70af7174a1184dccce32b"
|
||||
|
||||
highlight.js@9.4.0:
|
||||
version "9.4.0"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.4.0.tgz#2687d6cf6df0d57bc68585e836bfe3ab3edf9452"
|
||||
|
||||
history@3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/history/-/history-3.0.0.tgz#02cff4e6f69dc62dd81161104a63f5b85ead0c85"
|
||||
@@ -3965,7 +3949,7 @@ html-webpack-plugin@2.17.0:
|
||||
pretty-error "^2.0.0"
|
||||
toposort "^0.2.12"
|
||||
|
||||
htmlparser2@^3.9.0, htmlparser2@^3.9.1:
|
||||
htmlparser2@^3.9.1:
|
||||
version "3.9.2"
|
||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
|
||||
dependencies:
|
||||
@@ -4611,10 +4595,6 @@ isurl@^1.0.0-alpha5:
|
||||
has-to-string-tag-x "^1.2.0"
|
||||
is-object "^1.0.1"
|
||||
|
||||
iterate-object@^1.3.0, iterate-object@^1.3.1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/iterate-object/-/iterate-object-1.3.2.tgz#24ec15affa5d0039e8839695a21c2cae1f45b66b"
|
||||
|
||||
jest-changed-files@^20.0.3:
|
||||
version "20.0.3"
|
||||
resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-20.0.3.tgz#9394d5cc65c438406149bef1bf4d52b68e03e3f8"
|
||||
@@ -4870,10 +4850,6 @@ js-tokens@^3.0.0, js-tokens@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||
|
||||
js-tree@1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tree/-/js-tree-1.1.0.tgz#087ee3ec366a5b74eb14f486016c5e0e631f1670"
|
||||
|
||||
js-yaml@^3.4.3, js-yaml@^3.5.1, js-yaml@^3.7.0:
|
||||
version "3.9.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.9.1.tgz#08775cebdfdd359209f0d2acd383c8f86a6904a0"
|
||||
@@ -5720,12 +5696,6 @@ map-cache@^0.2.0:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
|
||||
|
||||
map-o@^2.0.1:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/map-o/-/map-o-2.0.7.tgz#7b59395ee87a5200ec2ef881938e9e257f747d61"
|
||||
dependencies:
|
||||
iterate-object "^1.3.0"
|
||||
|
||||
map-obj@^1.0.0, map-obj@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d"
|
||||
@@ -5734,12 +5704,6 @@ map-stream@~0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194"
|
||||
|
||||
marked-sanitized@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/marked-sanitized/-/marked-sanitized-0.1.1.tgz#8a5756887217f64fe92e1a92e71d0cc10e767829"
|
||||
dependencies:
|
||||
sanitize-html "^1.5.2"
|
||||
|
||||
marked-terminal@^1.6.2:
|
||||
version "1.7.0"
|
||||
resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-1.7.0.tgz#c8c460881c772c7604b64367007ee5f77f125904"
|
||||
@@ -5750,7 +5714,7 @@ marked-terminal@^1.6.2:
|
||||
lodash.assign "^4.2.0"
|
||||
node-emoji "^1.4.1"
|
||||
|
||||
marked@0.3.6, marked@^0.3.6:
|
||||
marked@^0.3.6:
|
||||
version "0.3.6"
|
||||
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.6.tgz#b2c6c618fccece4ef86c4fc6cb8a7cbf5aeda8d7"
|
||||
|
||||
@@ -7610,10 +7574,6 @@ regex-cache@^0.4.2:
|
||||
dependencies:
|
||||
is-equal-shallow "^0.1.3"
|
||||
|
||||
regexp-quote@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/regexp-quote/-/regexp-quote-0.0.0.tgz#1e0f4650c862dcbfed54fd42b148e9bb1721fcf2"
|
||||
|
||||
regexpu-core@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-1.0.0.tgz#86a763f58ee4d7c2f6b102e4764050de7ed90c6b"
|
||||
@@ -7846,14 +7806,6 @@ sane@~1.6.0:
|
||||
walker "~1.0.5"
|
||||
watch "~0.10.0"
|
||||
|
||||
sanitize-html@^1.5.2:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-1.14.1.tgz#730ffa2249bdf18333effe45b286173c9c5ad0b8"
|
||||
dependencies:
|
||||
htmlparser2 "^3.9.0"
|
||||
regexp-quote "0.0.0"
|
||||
xtend "^4.0.0"
|
||||
|
||||
sax@^1.2.1, sax@~1.2.1:
|
||||
version "1.2.4"
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
@@ -8084,8 +8036,8 @@ slate-edit-list@^0.7.0:
|
||||
resolved "https://registry.yarnpkg.com/slate-edit-list/-/slate-edit-list-0.7.1.tgz#84ee960d2d5b5a20ce267ad9df894395a91b93d5"
|
||||
|
||||
slate-markdown-serializer@tommoor/slate-markdown-serializer:
|
||||
version "0.4.3"
|
||||
resolved "https://codeload.github.com/tommoor/slate-markdown-serializer/tar.gz/8e987951db999617ff6759c85e384dad175d5b92"
|
||||
version "0.5.0"
|
||||
resolved "https://codeload.github.com/tommoor/slate-markdown-serializer/tar.gz/22bdaa096777f5dd7f7c366841a0c6e4392adeb6"
|
||||
|
||||
slate-paste-linkify@^0.2.1:
|
||||
version "0.2.1"
|
||||
@@ -8711,12 +8663,6 @@ trim-right@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"
|
||||
|
||||
"truncate-html@https://github.com/jorilallo/truncate-html/tarball/master":
|
||||
version "0.1.1"
|
||||
resolved "https://github.com/jorilallo/truncate-html/tarball/master#5856f297610d202045d997965fc8c33be453c2e9"
|
||||
dependencies:
|
||||
cheerio "0.22.0"
|
||||
|
||||
tryit@^1.0.1:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tryit/-/tryit-1.0.3.tgz#393be730a9446fd1ead6da59a014308f36c289cb"
|
||||
|
||||
Reference in New Issue
Block a user