Skip to content
This repository has been archived by the owner on Nov 28, 2022. It is now read-only.

Commit

Permalink
Server-Side Rendered RDMD (#578)
Browse files Browse the repository at this point in the history
* server-side render guide markdown

* pass tests

* add files field to package

whitelists the styles/ and components/ dir

* extremely simple server/client fork

for frontend-only libs (in this case CodeMirror...)

* update test snapshots

* add comments

* deps(markdown): add terser

* style(markdown)

* chore(markdown): set watch mode to dev explicitly
rafegoldberg authored Mar 31, 2020

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 1f7f16f commit af8f49f
Showing 22 changed files with 2,018 additions and 1,817 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Components Embed 1`] = `"<div class=\\"embed\\"><a href=\\"https://gist.github.com/chaddy81/f852004d6d1510eec1f6\\" rel=\\"noopener noreferrer\\" style=\\"display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; text-decoration: none;\\" target=\\"_blank\\"><b style=\\"color: rgb(51, 51, 51);\\">View Embed:</b> <span style=\\"opacity: 0.75;\\">https://gist.github.com/chaddy81/f852004d6d1510eec1f6</span></a></div>"`;
exports[`Components Multi Code Block 1`] = `
"<div class=\\"CodeTabs\\"><div class=\\"CodeTabs-toolbar\\"><button type=\\"button\\">(plaintext)</button><button type=\\"button\\" class=\\"CodeTabs_active\\">(plaintext)</button></div><div class=\\"CodeTabs-inner\\"><pre><code data-lang=\\"\\" name=\\"\\"><span class=\\"cm-s-neo\\">hello
</span></code></pre><pre class=\\"CodeTabs_active\\"><code data-lang=\\"\\" name=\\"\\"><span class=\\"cm-s-neo\\">world
</span></code></pre></div></div>"
`;
Original file line number Diff line number Diff line change
@@ -7,6 +7,37 @@ Object {
}
`;

exports[`Blank Magic Blocks 2`] = `
Object {
"children": Array [
Object {
"children": "",
"depth": 2,
"type": "heading",
},
],
"type": "root",
}
`;

exports[`Blank Magic Blocks 3`] = `
Object {
"children": Array [
Object {
"children": Array [
Object {
"type": "text",
"value": "No Level",
},
],
"depth": 2,
"type": "heading",
},
],
"type": "root",
}
`;

exports[`Parse Magic Blocks Callout Blocks 1`] = `
Object {
"children": Array [
4 changes: 1 addition & 3 deletions packages/markdown/__tests__/components.test.js
Original file line number Diff line number Diff line change
@@ -85,9 +85,7 @@ world

wrap.find('button').last().simulate('click').simulate('click');

expect(wrap.html()).toBe(
'<div class="CodeTabs"><div class="CodeTabs-toolbar"><button type="button">(plaintext)</button><button type="button" class="CodeTabs_active">(plaintext)</button></div><div class="CodeTabs-inner"><pre><code data-lang="" name=""><span class="cm-s-neo">hello\n</span></code></pre><pre class="CodeTabs_active"><code data-lang="" name=""><span class="cm-s-neo">world\n</span></code></pre></div></div>'
);
expect(wrap.html()).toMatchSnapshot();
});

it('Embed', () => {
15 changes: 13 additions & 2 deletions packages/markdown/__tests__/magic-block-parser.test.js
Original file line number Diff line number Diff line change
@@ -16,9 +16,20 @@ const process = (text, opts = options) =>
.parse(text);

test('Blank Magic Blocks', () => {
const text = `[block:api-header]
const blank = `[block:api-header]
{}
[/block]`;
expect(process(text)).toMatchSnapshot();
expect(process(blank)).toMatchSnapshot();

const noTitle = `[block:api-header]
{ "level": 2 }
[/block]`;
expect(process(noTitle)).toMatchSnapshot();

const noLevel = `[block:api-header]
{ "title": "No Level" }
[/block]`;
expect(process(noLevel)).toMatchSnapshot();
});

test('Sanitize Schema', () => {
2 changes: 1 addition & 1 deletion packages/markdown/components/Callout/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require('./style.scss');
// require('./style.scss');

const React = require('react');
const PropTypes = require('prop-types');
8 changes: 3 additions & 5 deletions packages/markdown/components/Callout/style.scss
Original file line number Diff line number Diff line change
@@ -44,9 +44,7 @@
}
}
}
#hub-content,
.markdown-body {
rdme-callout, .callout {
@include callout;
}

.callout.callout {
@include callout;
}
9 changes: 6 additions & 3 deletions packages/markdown/components/Code.jsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
const React = require('react');
const PropTypes = require('prop-types');
const syntaxHighlighter = require('@readme/syntax-highlighter');

// Only load CodeMirror in the browser, for SSR
// apps. Necessary because of people like this:
// https://github.com/codemirror/CodeMirror/issues/3701#issuecomment-164904534
const syntaxHighlighter = typeof window !== 'undefined' ? require('@readme/syntax-highlighter') : false;

function Code(props) {
const { className, children, lang, meta } = props;
const language = (className || '').replace('language-', '');

return (
<code className={language ? `lang-${language}` : null} data-lang={lang} name={meta}>
{syntaxHighlighter(children[0], language, { tokenizeVariables: true })}
{syntaxHighlighter ? syntaxHighlighter(children[0], language, { tokenizeVariables: true }) : children[0]}
</code>
);
}
2 changes: 1 addition & 1 deletion packages/markdown/components/CodeTabs/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require('./style.scss');
// require('./style.scss');

const React = require('react');
const PropTypes = require('prop-types');
26 changes: 15 additions & 11 deletions packages/markdown/components/CodeTabs/style.scss
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
@import '~@readme/syntax-highlighter/node_modules/codemirror/lib/codemirror.css';
@import '~@readme/syntax-highlighter/node_modules/codemirror/theme/neo.css';

@mixin CodeTabs {
$bgc-pre: #F6F8FA;
$bgc-bar: #EBEDEF;

color: #333;
background: #eaeaea;
border-radius: 3px !important;
overflow: hidden;

&-toolbar {
background: $bgc-bar;
display: flex;
flex-flow: row nowrap;
overflow: hidden;
@@ -16,12 +23,13 @@
}
> button {
white-space: nowrap;
transition: .123s ease;
}
}

pre {
border-radius: 0 0 3px 3px !important;
background: #f6f6f6;
background: $bgc-pre;
margin-bottom: 0;
&:not(.CodeTabs_active) { display: none }
}
@@ -43,7 +51,7 @@

&.CodeTabs_initial button:first-child,
button.CodeTabs_active {
background: #f6f6f6;
background: $bgc-pre;
color: black;
pointer-events: none;
}
@@ -57,14 +65,10 @@
}
}

#hub-content,
html[ng-app="hub"] .markdown-body,
#root > .App {
.CodeTabs {
@include CodeTabs;
pre {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.CodeTabs {
@include CodeTabs;
pre {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
}
18 changes: 14 additions & 4 deletions packages/markdown/components/Heading/index.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require('./style.scss');
// require('./style.scss');

const React = require('react');
const PropTypes = require('prop-types');
@@ -17,10 +17,14 @@ function generateHeadingId(e, anchors) {
return id;
}

function Heading(props) {
function Heading({ tag, ...props }) {
if (!props.children) return '';
const id = `section-${generateHeadingId(props.children[0], props.anchors)}`;
return React.createElement(props.tag, { className: 'heading header-scroll' }, [
const attrs = {
className: `heading heading-${props.level} header-scroll`,
align: props.align,
};
return React.createElement(tag, attrs, [
<div key={`heading-anchor-${id}`} className="heading-anchor anchor waypoint" id={id} />,
<div key={`heading-text-${id}`} className="heading-text">
{props.children}
@@ -32,13 +36,19 @@ function Heading(props) {

function CreateHeading(level, anchors) {
// eslint-disable-next-line react/display-name
return props => <Heading {...props} anchors={anchors} tag={`h${level}`} />;
return props => <Heading {...props} anchors={anchors} level={level} tag={`h${level}`} />;
}

Heading.propTypes = {
align: PropTypes.oneOf(['left', 'center', 'right', '']),
anchors: PropTypes.object,
children: PropTypes.array.isRequired,
level: PropTypes.number,
tag: PropTypes.string.isRequired,
};
Heading.defaultProps = {
align: '',
level: 2,
};

module.exports = (level, anchors) => CreateHeading(level, anchors);
2 changes: 1 addition & 1 deletion packages/markdown/components/Heading/style.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.markdown-body .heading.heading {
.heading.heading {
display: flex;
justify-content: flex-start;
align-items: center;
9 changes: 9 additions & 0 deletions packages/markdown/components/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export { default as GlossaryItem } from './GlossaryItem';
export { default as Code } from './Code';
export { default as Table } from './Table';
export { default as Anchor } from './Anchor';
export { default as Heading } from './Heading';
export { default as Callout } from './Callout';
export { default as CodeTabs } from './CodeTabs';
export { default as Image } from './Image';
export { default as Embed } from './Embed';
64 changes: 30 additions & 34 deletions packages/markdown/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
require('./styles/main.scss');

const React = require('react');
const unified = require('unified');

@@ -22,33 +20,29 @@ const rehypeReact = require('rehype-react');
/* React Custom Components
*/
const Variable = require('@readme/variable');
const GlossaryItem = require('./components/GlossaryItem');
const Code = require('./components/Code');
const Table = require('./components/Table');
const Anchor = require('./components/Anchor');
const Heading = require('./components/Heading');
const Callout = require('./components/Callout');
const CodeTabs = require('./components/CodeTabs');
const Image = require('./components/Image');
const Embed = require('./components/Embed');
const { GlossaryItem, Code, Table, Anchor, Heading, Callout, CodeTabs, Image, Embed } = require('./components');

/* Custom Unified Parsers
*/
const flavorCodeTabs = require('./processor/parse/flavored/code-tabs');
const flavorCallout = require('./processor/parse/flavored/callout');
const flavorEmbed = require('./processor/parse/flavored/embed');
const magicBlockParser = require('./processor/parse/magic-block-parser');
const variableParser = require('./processor/parse/variable-parser');
const gemojiParser = require('./processor/parse/gemoji-parser');
const {
flavorCodeTabs,
flavorCallout,
flavorEmbed,
magicBlockParser,
variableParser,
gemojiParser,
} = require('./processor/parse');

/* Custom Unified Compilers
*/
const rdmeDivCompiler = require('./processor/compile/div');
const codeTabsCompiler = require('./processor/compile/code-tabs');
const rdmeEmbedCompiler = require('./processor/compile/embed');
const rdmeVarCompiler = require('./processor/compile/var');
const rdmeCalloutCompiler = require('./processor/compile/callout');
const rdmePinCompiler = require('./processor/compile/pin');
const {
rdmeDivCompiler,
codeTabsCompiler,
rdmeEmbedCompiler,
rdmeVarCompiler,
rdmeCalloutCompiler,
rdmePinCompiler,
} = require('./processor/compile');

// Processor Option Defaults
const options = require('./options.json');
@@ -57,7 +51,7 @@ const options = require('./options.json');
sanitize.clobberPrefix = '';

sanitize.tagNames.push('span');
sanitize.attributes['*'].push('class', 'className');
sanitize.attributes['*'].push('class', 'className', 'align');

sanitize.tagNames.push('rdme-pin');

@@ -99,7 +93,7 @@ export const utils = {
/**
* Core markdown text processor
*/
function parseMarkdown(opts = {}) {
export function processor(opts = {}) {
/*
* This is kinda complicated: "markdown" within ReadMe is
* often more than just markdown. It can also include HTML,
@@ -131,31 +125,32 @@ function parseMarkdown(opts = {}) {
.use(rehypeSanitize, sanitize);
}

export function plain(text, opts = options) {
export function plain(text, opts = options, components = {}) {
if (!text) return null;
return parseMarkdown(opts)
return processor(opts)
.use(rehypeReact, {
createElement: React.createElement,
Fragment: React.Fragment,
components,
})
.processSync(opts.normalize ? normalize(text) : text).contents;
}

/**
* return a React VDOM component tree
*/
export function react(text, opts = options) {
export function react(text, opts = options, components = {}) {
if (!text) return null;

// eslint-disable-next-line react/prop-types
const PinWrap = ({ children }) => <div className="pin">{children}</div>;
const count = {};

return parseMarkdown(opts)
return processor(opts)
.use(rehypeReact, {
createElement: React.createElement,
Fragment: React.Fragment,
components: {
components: (typeof components === 'function' ? components : r => r)({
'code-tabs': CodeTabs(sanitize),
'rdme-callout': Callout(sanitize),
'readme-variable': Variable,
@@ -172,7 +167,8 @@ export function react(text, opts = options) {
h6: Heading(6, count),
code: Code(sanitize),
img: Image(sanitize),
},
...components,
}),
})
.processSync(opts.normalize ? normalize(text) : text).contents;
}
@@ -183,7 +179,7 @@ export function react(text, opts = options) {
export function html(text, opts = options) {
if (!text) return null;

return parseMarkdown(opts)
return processor(opts)
.use(rehypeStringify)
.processSync(opts.normalize ? normalize(text) : text).contents;
}
@@ -193,7 +189,7 @@ export function html(text, opts = options) {
*/
export function ast(text, opts = options) {
if (!text) return null;
return parseMarkdown(opts)
return processor(opts)
.use(remarkStringify, opts.markdownOptions)
.parse(opts.normalize ? normalize(text) : text);
}
@@ -203,7 +199,7 @@ export function ast(text, opts = options) {
*/
export function md(tree, opts = options) {
if (!tree) return null;
return parseMarkdown(opts)
return processor(opts)
.use(remarkStringify, opts.markdownOptions)
.use([rdmeDivCompiler, codeTabsCompiler, rdmeCalloutCompiler, rdmeEmbedCompiler, rdmeVarCompiler, rdmePinCompiler])
.stringify(tree);
3,342 changes: 1,706 additions & 1,636 deletions packages/markdown/package-lock.json

Large diffs are not rendered by default.

12 changes: 9 additions & 3 deletions packages/markdown/package.json
Original file line number Diff line number Diff line change
@@ -3,6 +3,10 @@
"main": "dist/main.js",
"description": "ReadMe's React-based Markdown parser",
"version": "6.1.0",
"files": [
"styles",
"components"
],
"dependencies": {
"@readme/emojis": "^1.0.0",
"@readme/syntax-highlighter": "^6.0.15",
@@ -24,8 +28,8 @@
"unist-util-visit": "^2.0.1"
},
"scripts": {
"build": "webpack --config webpack.config.js",
"watch": "webpack -w --progress",
"build": "webpack --config webpack.prod.js",
"watch": "webpack -w --progress --mode development",
"lint": "eslint . --ext .jsx --ext .js",
"inspect": "jsinspect",
"pretest": "npm run lint && npm run prettier && npm run inspect",
@@ -47,7 +51,9 @@
"jest": "^25.1.0",
"jsinspect": "^0.12.7",
"prettier": "^2.0.1",
"webpack": "^3.10.0"
"terser-webpack-plugin": "^2.3.5",
"webpack": "^4.41.0",
"webpack-merge": "^4.2.2"
},
"prettier": "@readme/eslint-config/prettier"
}
6 changes: 6 additions & 0 deletions packages/markdown/processor/compile/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { default as rdmeDivCompiler } from './div';
export { default as codeTabsCompiler } from './code-tabs';
export { default as rdmeEmbedCompiler } from './embed';
export { default as rdmeVarCompiler } from './var';
export { default as rdmeCalloutCompiler } from './callout';
export { default as rdmePinCompiler } from './pin';
6 changes: 6 additions & 0 deletions packages/markdown/processor/parse/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { default as flavorCodeTabs } from './flavored/code-tabs';
export { default as flavorCallout } from './flavored/callout';
export { default as flavorEmbed } from './flavored/embed';
export { default as magicBlockParser } from './magic-block-parser';
export { default as variableParser } from './variable-parser';
export { default as gemojiParser } from './gemoji-parser';
3 changes: 3 additions & 0 deletions packages/markdown/styles/components.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@import '../components/Callout/style.scss';
@import '../components/CodeTabs/style.scss';
@import '../components/Heading/style.scss';
102 changes: 0 additions & 102 deletions packages/markdown/styles/gfm.overrides.scss

This file was deleted.

137 changes: 130 additions & 7 deletions packages/markdown/styles/gfm.scss
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
@mixin markdown-body {
@mixin gfm {
@include markdown-body-defaults;
@include markdown-body-overrides;
}

@mixin markdown-body-defaults {
& {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
position: relative;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
line-height: 1.5;
line-height: 1.5;
color: mix(#6d757c, #24292e, 12%);
color: mix(#6d757c, #24292e, 40%);
word-wrap: break-word
}

@@ -507,7 +512,7 @@
display: table
}

&>:first-child {
& > :first-child {
margin-top: 0 !important
}

@@ -728,18 +733,22 @@
& {
margin: 15px auto;
}

figcaption {
font-size: 0.88em;
font-style: italic;
}

&[align=right] {
float: right;
margin-left: 15px;
}

&[align=left] {
float: left;
margin-right: 15px;
}

figcaption {
font-size: small;
text-align: center;
@@ -996,8 +1005,122 @@
}
}

.field-description,
.markdown-body {
@include markdown-body;
font-size: 15px;
@mixin markdown-body-overrides {

h5,
h6 {
font-size: .9em;
}


blockquote h1:last-child,
blockquote h2:last-child {
border-bottom: 0;
}


>* {
margin-top: 15px;
margin-bottom: 15px !important;
}


img[align=left] {
margin-right: 20px;
}

img[align=right] {
margin-left: 20px;
}

img[align=center] {
display: block;
margin: 20px auto;
}


.task-list-item input {
margin: 0 .5em .25em -1.25em;
}

a {
transition: .3s;
text-decoration: underline;

&:not(:hover) {
text-decoration-color: transparent;
}

code {
display: inline-block;
text-decoration: underline;
transition: .3s;
}

&:hover code {
color: inherit;
}

&:not(:hover) code {
text-decoration-color: transparent;
}
}

.embed {
padding: 15px;
background: #eeeeee;
overflow: hidden;
border-radius: 3px;

&:empty {
display: none;
}

&-media {
display: flex;
justify-content: center;

>:only-child {
flex: 1;
margin: -15px;
border-radius: 0 !important;
}
}
}

.table {
display: inline-block;

>table {
margin-bottom: 0 !important;
}

&_centered {
margin: 30px auto !important;
display: block;
}

&_rounded {
border: 1px solid #e0e2e5;
border-radius: 3px;
overflow: hidden;

tr,
tr td,
tr th {
border-width: 0 !important;
border-color: #e0e2e5;
}

thead+tbody tr:first-of-type,
tr+tr {
border-top-width: 1px !important;
}

tr td+td,
tr th+th {
border-left-width: 1px !important;
}
}
}
}
13 changes: 9 additions & 4 deletions packages/markdown/styles/main.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
@import '~@readme/syntax-highlighter/node_modules/codemirror/lib/codemirror.css';
@import '~@readme/syntax-highlighter/node_modules/codemirror/theme/neo.css';

@import './gfm.scss';
@import './gfm.overrides.scss';
@import './components.scss';

// #hub-content != html[ng-app="hub"] .markdown-body != #root > .App != #app

.field-description,
.markdown-body {
@include gfm;
font-size: 14px;
}
18 changes: 18 additions & 0 deletions packages/markdown/webpack.prod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true }] */
const webpack = require('webpack');
const merge = require('webpack-merge');
const TerserPlugin = require('terser-webpack-plugin');

const common = require('./webpack.config');

module.exports = merge(common, {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
],
});

0 comments on commit af8f49f

Please sign in to comment.