Skip to content

Commit

Permalink
Initial implementation of a blog support
Browse files Browse the repository at this point in the history
Summary:This adds blog post rendering, left nav with the list of blog posts, highlighting the currently selected blog post and highlighting the header links.

I cp all the React blog posts in order to test it out, working great.

Missing pieces:
 - I haven't implemented a listing of all blog posts, the header link opens the most recent one.
 - I haven't implemented truncation of the links on the left and the all page

Those are needed when we'll have > 1 blog post, which is not now :)

bypass-lint

![screen shot 2016-03-02 at 3 55 09 pm](https://cloud.githubusercontent.com/assets/197597/13480193/011abff8-e091-11e5-8421-677217f383ff.png)
Closes jestjs#760

Differential Revision: D3006427

fb-gh-sync-id: 0e635247732f6187f9255ea92d59701f1a467af4
shipit-source-id: 0e635247732f6187f9255ea92d59701f1a467af4
  • Loading branch information
vjeux authored and Facebook Github Bot 3 committed Mar 3, 2016
1 parent edcd074 commit 4bd98ed
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 52 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@
/website/node_modules
npm-debug.log
build
website/core/metadata.js
website/core/metadata*.js
website/src/jest/docs
website/src/jest/blog/2*
8 changes: 7 additions & 1 deletion website/core/HeaderLinks.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @providesModule HeaderLinks
* @jsx React.DOM
*/

var MetadataBlog = require('MetadataBlog');
var React = require('React');

var HeaderLinks = React.createClass({
links: [
{section: 'docs', href: '/jest/docs/tutorial.html#content', text: 'docs'},
{section: 'support', href: '/jest/support.html', text: 'support'},
MetadataBlog.files.length > 0 && {section: 'blog', href: '/jest/blog/' + MetadataBlog.files[0].path, text: 'blog'},
{section: 'github', href: 'http://github.com/facebook/jest', text: 'github'},
],
].filter(function(e) { return e; }),

render: function() {
return (
Expand Down
66 changes: 66 additions & 0 deletions website/layout/BlogPostLayout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* Copyright 2004-present Facebook. All Rights Reserved.
*
* @providesModule BlogPostLayout
* @jsx React.DOM
*/

var Marked = require('Marked');
var MetadataBlog = require('MetadataBlog');
var React = require('React');
var Site = require('Site');

var BlogPostLayout = React.createClass({
renderSidebar: function(current) {
return (
<div className="nav-docs">
<div className="nav-docs-section">
<h3>Recent Posts</h3>
<ul>
{MetadataBlog.files.map(function(post) {
return (
<li key={post.path}>
<a
className={current.title === post.title ? 'active' : ''}
href={'/jest/blog/' + post.path}>
{post.title}
</a>
</li>
);
})}
</ul>
</div>
</div>
);
},

render: function() {
var metadata = this.props.metadata;
var content = this.props.children;

var match = metadata.path.match(/([0-9]+)\/([0-9]+)\/([0-9]+)/);
// Because JavaScript sucks at date handling :(
var year = match[1];
var month = [
'January', 'February', 'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October', 'November', 'December'
][parseInt(match[2], 10) + 1];
var day = parseInt(match[3], 10);

return (
<Site section="blog">
<section className="content wrap documentationContent">
{this.renderSidebar(this.props.metadata)}
<div className="inner-content">
<h1>{metadata.title}</h1>
<p className="meta">{month} {day}, {year} by {metadata.author}</p>
<hr />
<Marked>{content}</Marked>
</div>
</section>
</Site>
);
}
});

module.exports = BlogPostLayout;
165 changes: 115 additions & 50 deletions website/server/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,27 +18,81 @@ function splitHeader(content) {
};
}

function globEach(pattern, cb) {
glob(pattern, function(err, files) {
if (err) {
console.error(err);
return;
}
files.forEach(cb);
});
}

function rmFile(file) {
try {
fs.unlinkSync(file);
} catch(e) {
/* seriously, unlink throws when the file doesn't exist :( */
}
}

function backtickify(str) {
var escaped = '`' + str.replace(/\\/g, '\\\\').replace(/`/g, '\\`') + '`';
// Replace require( with require\( so node-haste doesn't replace example
// require calls in the docs
return escaped.replace(/require\(/g, 'require\\(');
}


// Extract markdown metadata header
function extractMetadata(content) {
var metadata = {};
var both = splitHeader(content);
var lines = both.header.split('\n');
for (var i = 0; i < lines.length - 1; ++i) {
var keyvalue = lines[i].split(':');
var key = keyvalue[0].trim();
var value = keyvalue.slice(1).join(':').trim();
// Handle the case where you have "Community #10"
try { value = JSON.parse(value); } catch(e) { }
metadata[key] = value;
}
return {metadata: metadata, rawContent: both.content};
}

function buildFile(layout, rawContent, metadata) {
return (
'/**\n' +
' * @generated\n' +
' */\n' +
'var React = require("React");\n' +
'var Layout = require("' + layout + '");\n' +
'var content = ' + backtickify(rawContent) + '\n' +
'var Post = React.createClass({\n' +
' statics: {\n' +
' content: content\n' +
' },\n' +
' render: function() {\n' +
' return <Layout metadata={' + JSON.stringify(metadata) + '}>{content}</Layout>;\n' +
' }\n' +
'});\n' +
'module.exports = Post;\n'
);
}

function writeFileAndCreateFolder(file, content) {
mkdirp.sync(file.replace(new RegExp('/[^/]*$'), ''));
fs.writeFileSync(file, content);
}

function execute() {
var MD_DIR = '../docs/';
var DOCS_MD_DIR = '../docs/';
var BLOG_MD_DIR = '../blog/';

glob('src/jest/docs/*.*', function(er, files) {
files.forEach(function(file) {
try {
fs.unlinkSync(file);
} catch(e) {
/* seriously, unlink throws when the file doesn't exist :( */
}
});
});
globEach('src/jest/docs/*.*', rmFile);
globEach('src/jest/blog/*.*', rmFile);

var api = splitHeader(fs.readFileSync(MD_DIR + 'API.md', {encoding: 'utf8'}).toString()).content
var api = splitHeader(fs.readFileSync(DOCS_MD_DIR + 'API.md', {encoding: 'utf8'}).toString()).content
.split('---')[0]
.replace(/\(#/g, '(http://facebook.github.io/jest/docs/api.html#');
var readme = fs.readFileSync('../README.md', {encoding: 'utf8'}).toString()
Expand All @@ -48,29 +102,17 @@ function execute() {
);
fs.writeFileSync('../README.md', readme);

glob(DOCS_MD_DIR + '**/*.*', function(er, files) {
var metadatas = {
files: [],
};

var metadatas = {
files: [],
};

glob(MD_DIR + '**/*.*', function (er, files) {
files.forEach(function(file) {
var extension = path.extname(file);
if (extension === '.md' || extension === '.markdown') {
var content = fs.readFileSync(file, {encoding: 'utf8'});
var metadata = {};

// Extract markdown metadata header
var both = splitHeader(content);
var lines = both.header.split('\n');
for (var i = 0; i < lines.length - 1; ++i) {
var keyvalue = lines[i].split(':');
var key = keyvalue[0].trim();
var value = keyvalue.slice(1).join(':').trim();
// Handle the case where you have "Community #10"
try { value = JSON.parse(value); } catch(e) { }
metadata[key] = value;
}
var res = extractMetadata(fs.readFileSync(file, {encoding: 'utf8'}));
var metadata = res.metadata;
var rawContent = res.rawContent;
metadatas.files.push(metadata);

if (metadata.permalink.match(/^https?:/)) {
Expand All @@ -80,27 +122,10 @@ function execute() {
// Create a dummy .js version that just calls the associated layout
var layout = metadata.layout[0].toUpperCase() + metadata.layout.substr(1) + 'Layout';

var content = (
'/**\n' +
' * @generated\n' +
' */\n' +
'var React = require("React");\n' +
'var Layout = require("' + layout + '");\n' +
'var content = ' + backtickify(both.content) + '\n' +
'var Post = React.createClass({\n' +
' statics: {\n' +
' content: content\n' +
' },\n' +
' render: function() {\n' +
' return <Layout metadata={' + JSON.stringify(metadata) + '}>{content}</Layout>;\n' +
' }\n' +
'});\n' +
'module.exports = Post;\n'
writeFileAndCreateFolder(
'src/jest/' + metadata.permalink.replace(/\.html$/, '.js'),
buildFile(layout, rawContent, metadata)
);

var targetFile = 'src/jest/' + metadata.permalink.replace(/\.html$/, '.js');
mkdirp.sync(targetFile.replace(new RegExp('/[^/]*$'), ''));
fs.writeFileSync(targetFile, content);
}

if (extension === '.json') {
Expand All @@ -118,6 +143,46 @@ function execute() {
'module.exports = ' + JSON.stringify(metadatas, null, 2) + ';'
);
});

glob(BLOG_MD_DIR + '**/*.*', function(er, files) {
var metadatas = {
files: [],
};

files.sort().reverse().forEach(function(file) {
// Transform
// 2015-08-13-blog-post-name-0.5.md
// into
// 2015/08/13/blog-post-name-0-5.html
var filePath = path.basename(file)
.replace('-', '/')
.replace('-', '/')
.replace('-', '/')
// react-middleware is broken with files that contains multiple . like react-0.14.js
.replace(/\./g, '-')
.replace(/\-md$/, '.html');

var res = extractMetadata(fs.readFileSync(file, {encoding: 'utf8'}));
var metadata = Object.assign({path: filePath}, res.metadata);
var rawContent = res.rawContent;

metadatas.files.push(metadata);

writeFileAndCreateFolder(
'src/jest/blog/' + filePath.replace(/\.html$/, '.js'),
buildFile('BlogPostLayout', rawContent, metadata)
);
});

fs.writeFileSync(
'core/metadata-blog.js',
'/**\n' +
' * @generated\n' +
' * @providesModule MetadataBlog\n' +
' */\n' +
'module.exports = ' + JSON.stringify(metadatas, null, 2) + ';'
);
});
}

if (argv.convert) {
Expand Down

0 comments on commit 4bd98ed

Please sign in to comment.