Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

An assortment of Markdown Editor fixes #198

Merged
merged 10 commits into from
Dec 30, 2016
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
dist/
bin/
node_modules/
npm-debug.log
.DS_Store
.tern-project
yarn-error.log
.vscode/
.vscode/
2 changes: 1 addition & 1 deletion example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
window.repoFiles = {
_posts: {
"2015-02-14-this-is-a-post.md": {
content: "---\ntitle: This is a post\nimage: /nf-logo.png\ndate: 2015-02-14T00:00:00.000Z\n---\n\n# I Am a Title in Markdown\n\nHello, world\n"
content: "---\ntitle: This is a post\nimage: /nf-logo.png\ndate: 2015-02-14T00:00:00.000Z\n---\n\n# I Am a Title in Markdown\n\nHello, world\n\n* One Thing\n* Another Thing\n* A Third Thing\n"
}
},
_faqs: {
Expand Down
28 changes: 16 additions & 12 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"test": "jest",
"test:watch": "jest --watch",
"build": "NODE_ENV=production webpack --config webpack.prod.js",
"build:scripts": "NODE_ENV=production webpack --config webpack.cli.js",
"prepublish": "npm run build",
"storybook": "start-storybook -p 9001",
"storybook-build": "build-storybook -o dist",
Expand Down Expand Up @@ -91,7 +92,10 @@
"dependencies": {
"@kadira/storybook": "^1.36.0",
"autoprefixer": "^6.3.3",
"babel": "^6.5.2",
"babel-cli": "^6.18.0",
"dateformat": "^1.0.12",
"deep-equal": "^1.0.1",
"fuzzy": "^0.1.1",
"immutability-helper": "^2.0.0",
"immutable": "^3.7.6",
Expand All @@ -108,18 +112,18 @@
"normalize.css": "^4.2.0",
"pluralize": "^3.0.0",
"prismjs": "^1.5.1",
"prosemirror-commands": "^0.12.0",
"prosemirror-history": "^0.12.0",
"prosemirror-inputrules": "^0.12.0",
"prosemirror-keymap": "^0.12.0",
"prosemirror-markdown": "^0.12.0",
"prosemirror-model": "^0.12.0",
"prosemirror-schema-basic": "^0.12.0",
"prosemirror-schema-list": "^0.12.0",
"prosemirror-schema-table": "^0.12.0",
"prosemirror-state": "^0.12.0",
"prosemirror-transform": "^0.12.1",
"prosemirror-view": "^0.12.0",
"prosemirror-commands": "^0.16.0",
"prosemirror-history": "^0.16.0",
"prosemirror-inputrules": "^0.16.0",
"prosemirror-keymap": "^0.16.0",
"prosemirror-markdown": "^0.16.0",
"prosemirror-model": "^0.16.0",
"prosemirror-schema-basic": "^0.16.0",
"prosemirror-schema-list": "^0.16.0",
"prosemirror-schema-table": "^0.16.0",
"prosemirror-state": "^0.16.0",
"prosemirror-transform": "^0.16.0",
"prosemirror-view": "^0.16.0",
"react": "^15.1.0",
"react-addons-css-transition-group": "^15.3.1",
"react-autosuggest": "^7.0.1",
Expand Down
150 changes: 150 additions & 0 deletions scripts/autoconfigure.collection.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import fs from "fs";
import path from "path";
import process from "process";
import yaml from 'js-yaml';
import deepEqual from 'deep-equal';
import { formatByExtension } from "../src/formats/formats";

const looksLikeMarkdown = /(\[.+\]\(.+\)|\n?[#]+ [a-z0-9])/; // eslint-disable-line
const looksLikeAnImage = /^[^ ]+\.(png|jpg|svg|gif|jpeg)/;

function capitalize(name) {
return name.substr(0, 1).toUpperCase() + name.substr(1);
}

function inferWidget(name, value) {
if (value == null) {
return { widget: 'string' };
}
if (value instanceof Date) {
return { widget: value.toJSON().match(/T00:00:00\.000Z$/) ? 'date' : 'datetime' };
}
if (value instanceof Array) {
if (typeof value[0] === 'string') {
return { widget: 'list' };
}
return { widget: 'list', fields: inferFields(value) };
}
if (typeof value === 'object') {
return { widget: 'object', fields: inferFields([value]) };
}
if (value === false || value === true) {
return { widget: 'checkbox' };
}
if (typeof value === 'number') {
return { widget: 'number' };
}
if (name === 'body' || value.match(looksLikeMarkdown)) {
return { widget: 'markdown' };
}
if (value.match(/\n/)) {
return { widget: 'text' };
}
if (value.match(looksLikeAnImage)) {
return { widget: 'image' };
}
return { widget: 'string' };
}

function inferField(name, value) {
return Object.assign({
label: capitalize(name.replace(/_-/g, ' ')),
name,
}, inferWidget(name, value));
}

function inferFields(entries) {
const fields = {};
entries.forEach((entry) => {
if (entry == null) { return; }
Object.keys(entry).forEach((fieldName) => {
const field = inferField(fieldName, entry[fieldName]);
if (fields[fieldName]) {
fields[fieldName] = combineFields(fields[fieldName], field);
} else {
fields[fieldName] = field;
}
});
});
return Object.keys(fields).map(key => fields[key]);
}

const widgetRank = {
markdown: 1,
text: 2,
string: 3,
image: 4,
datetime: 4,
date: 5,
number: 5,
object: 7,
list: 7,
};

function compareWidget(a, b) {
return widgetRank[a] - widgetRank[b];
}

function combineFields(a, b) {
if (b == null && a) {
return a;
}
if (a == null && b) {
return b;
}
if (deepEqual(a, b)) {
return a;
}
if (a.widget === b.widget) {
if (a.fields && b.fields) {
const newFields = {};
a.fields.forEach((field) => {
newFields[field.name] = combineFields(field, b.fields.find(f => f.name === field.name));
});
b.fields.forEach((field) => {
if (!newFields[field.name]) {
newFields[field.name] = field;
}
});
return Object.assign({}, a, { fields: Object.keys(newFields).map(k => newFields[k]) });
}
return a;
}
return [a, b].sort((fieldA, fieldB) => compareWidget(fieldB.widget, fieldA.widget))[0];
}

if (process.argv.length !== 3) {
console.log("Usage: autoconfigure.collections.js <path-to-my-folder>");
process.exit(1);
}

const folder = process.argv[2].replace(/\/$/, '');
const files = fs.readdirSync(folder);
const extensions = {};

files.forEach((file) => {
const ext = file.split(".").pop();
if (ext) {
extensions[ext] = extensions[ext] || 0;
extensions[ext] += 1;
}
});

const name = folder.split('/').filter(s => s).pop();
const extension = Object.keys(extensions).sort((a, b) => extensions[b] - extensions[a])[0];
const format = formatByExtension(extension);
const entries = files.filter(name => name.split(".").pop() === extension).slice(0, 100).map(file => (
format.fromFile(fs.readFileSync(path.join(folder, file), { encoding: 'utf8' }))
));
const fields = inferFields(entries);


const collection = {
label: capitalize(name),
name,
folder,
extension,
fields,
};

console.log(yaml.safeDump([collection], { flowLevel: 3 }));
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@ import htmlSyntax from 'markup-it/syntaxes/html';
import reInline from 'markup-it/syntaxes/markdown/re/inline';
import MarkupItReactRenderer from '../';

function getMedia(path) {
return path;
}

describe('MarkitupReactRenderer', () => {
describe('basics', () => {
it('should re-render properly after a value and syntax update', () => {
const component = shallow(
<MarkupItReactRenderer
value="# Title"
syntax={markdownSyntax}
getMedia={getMedia}
/>
);
const tree1 = component.html();
Expand All @@ -33,6 +38,7 @@ describe('MarkitupReactRenderer', () => {
<MarkupItReactRenderer
value="# Title"
syntax={markdownSyntax}
getMedia={getMedia}
/>
);
const syntax1 = component.instance().props.syntax;
Expand Down Expand Up @@ -77,6 +83,7 @@ Text with **bold** & _em_ elements
<MarkupItReactRenderer
value={value}
syntax={markdownSyntax}
getMedia={getMedia}
/>
);
expect(component.html()).toMatchSnapshot();
Expand All @@ -91,6 +98,7 @@ Text with **bold** & _em_ elements
<MarkupItReactRenderer
value={value}
syntax={markdownSyntax}
getMedia={getMedia}
/>
);
expect(component.html()).toMatchSnapshot();
Expand All @@ -115,6 +123,7 @@ Text with **bold** & _em_ elements
<MarkupItReactRenderer
value={value}
syntax={markdownSyntax}
getMedia={getMedia}
/>
);
expect(component.html()).toMatchSnapshot();
Expand All @@ -134,6 +143,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]
<MarkupItReactRenderer
value={value}
syntax={markdownSyntax}
getMedia={getMedia}
/>
);
expect(component.html()).toMatchSnapshot();
Expand All @@ -147,6 +157,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]
<MarkupItReactRenderer
value={value}
syntax={markdownSyntax}
getMedia={getMedia}
/>
);
expect(component.html()).toMatchSnapshot();
Expand All @@ -158,6 +169,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]
<MarkupItReactRenderer
value={value}
syntax={markdownSyntax}
getMedia={getMedia}
/>
);
expect(component.html()).toMatchSnapshot();
Expand All @@ -172,7 +184,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]
<form action="test">
<label for="input">
<input type="checkbox" checked="checked" id="input"/> My label
</label>
</label>
<dl class="test-class another-class" style="width: 100%">
<dt data-attr="test">Test HTML content</dt>
<dt>Testing HTML in Markdown</dt>
Expand All @@ -185,6 +197,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]
<MarkupItReactRenderer
value={value}
syntax={markdownSyntax}
getMedia={getMedia}
/>
);
expect(component.html()).toMatchSnapshot();
Expand Down Expand Up @@ -228,6 +241,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]
value={value}
syntax={myMarkdownSyntax}
schema={myCustomSchema}
getMedia={getMedia}
/>
);
expect(component.html()).toMatchSnapshot();
Expand All @@ -241,6 +255,7 @@ I get 10 times more traffic from [Google] [1] than from [Yahoo] [2] or [MSN] [3]
<MarkupItReactRenderer
value={value}
syntax={htmlSyntax}
getMedia={getMedia}
/>
);
expect(component.html()).toMatchSnapshot();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ exports[`MarkitupReactRenderer Markdown rendering HTML should render HTML as is
"<article><h1>Title</h1><div><form action=\"test\">
<label for=\"input\">
<input type=\"checkbox\" checked=\"checked\" id=\"input\"/> My label
</label>
</label>
<dl class=\"test-class another-class\" style=\"width: 100%\">
<dt data-attr=\"test\">Test HTML content</dt>
<dt>Testing HTML in Markdown</dt>
Expand Down
22 changes: 15 additions & 7 deletions src/components/MarkupItReactRenderer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,7 @@ const defaultSchema = {
[ENTITIES.HARD_BREAK]: 'br',
};

const notAllowedAttributes = ['loose'];

function sanitizeProps(props) {
return omit(props, notAllowedAttributes);
}
const notAllowedAttributes = ['loose', 'image'];

export default class MarkupItReactRenderer extends React.Component {

Expand All @@ -66,6 +62,17 @@ export default class MarkupItReactRenderer extends React.Component {
}
}

sanitizeProps(props) {
const { getMedia } = this.props;

if (props.image) {
props = Object.assign({}, props, { src: getMedia(props.image).toString() });
}

return omit(props, notAllowedAttributes);
}


renderToken(schema, token, index = 0, key = '0') {
const type = token.get('type');
const data = token.get('data');
Expand All @@ -85,7 +92,7 @@ export default class MarkupItReactRenderer extends React.Component {
if (nodeType !== null) {
let props = { key, token };
if (typeof nodeType !== 'function') {
props = { key, ...sanitizeProps(data.toJS()) };
props = { key, ...this.sanitizeProps(data.toJS()) };
}
// If this is a react element
return React.createElement(nodeType, props, children);
Expand All @@ -108,7 +115,7 @@ export default class MarkupItReactRenderer extends React.Component {


render() {
const { value, schema } = this.props;
const { value, schema, getMedia } = this.props;
const content = this.parser.toContent(value);
return this.renderToken({ ...defaultSchema, ...schema }, content.get('token'));
}
Expand All @@ -121,4 +128,5 @@ MarkupItReactRenderer.propTypes = {
PropTypes.string,
PropTypes.func,
])),
getMedia: PropTypes.func.isRequired,
};
Loading