chore: move to monorepo (#1297)

* chore: move to monorepo

* lint all js file

* simplify circleCI

* fix failing tests

* fix tests due to folder rename

* fix test since v1 website is renamed
This commit is contained in:
Endilie Yacop Sucipto 2019-03-23 14:21:36 +07:00 committed by GitHub
parent 6b1d2e8c9c
commit 1f91d19a8c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
619 changed files with 12713 additions and 26817 deletions

View file

@ -0,0 +1,77 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const BlogPost = require('./BlogPost.js');
const BlogSidebar = require('./BlogSidebar.js');
const Container = require('./Container.js');
const MetadataBlog = require('./MetadataBlog.js');
const Site = require('./Site.js');
const utils = require('./utils.js');
// used to generate entire blog pages, i.e. collection of truncated blog posts
class BlogPageLayout extends React.Component {
getPageURL(page) {
let url = `${this.props.config.baseUrl}blog/`;
if (page > 0) {
url += `page${page + 1}/`;
}
return url;
}
render() {
const perPage = this.props.metadata.perPage;
const page = this.props.metadata.page;
return (
<Site
title="Blog"
language="en"
config={this.props.config}
className="blog"
metadata={{blog: true, blogListing: true}}>
<div className="docMainWrapper wrapper">
<BlogSidebar
language={this.props.language}
config={this.props.config}
/>
<Container className="mainContainer postContainer blogContainer">
<div className="posts">
{MetadataBlog.slice(page * perPage, (page + 1) * perPage).map(
post => (
<BlogPost
post={post}
content={post.content}
truncate
key={
utils.getPath(post.path, this.props.config.cleanUrl) +
post.title
}
config={this.props.config}
/>
),
)}
<div className="docs-prevnext">
{page > 0 && (
<a className="docs-prev" href={this.getPageURL(page - 1)}>
Prev
</a>
)}
{MetadataBlog.length > (page + 1) * perPage && (
<a className="docs-next" href={this.getPageURL(page + 1)}>
Next
</a>
)}
</div>
</div>
</Container>
</div>
</Site>
);
}
}
module.exports = BlogPageLayout;

View file

@ -0,0 +1,132 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const MarkdownBlock = require('./MarkdownBlock.js');
const utils = require('./utils.js');
// inner blog component for the article itself, without sidebar/header/footer
class BlogPost extends React.Component {
renderContent() {
if (this.props.truncate) {
return (
<article className="post-content">
<MarkdownBlock>
{utils.extractBlogPostBeforeTruncate(this.props.content)}
</MarkdownBlock>
{utils.blogPostHasTruncateMarker(this.props.content) && (
<div className="read-more">
<a
className="button"
href={`${this.props.config.baseUrl}blog/${utils.getPath(
this.props.post.path,
this.props.config.cleanUrl,
)}`}>
Read More
</a>
</div>
)}
</article>
);
}
return <MarkdownBlock>{this.props.content}</MarkdownBlock>;
}
renderAuthorPhoto() {
const post = this.props.post;
const className = `authorPhoto${
post.author && post.authorTitle ? ' authorPhotoBig' : ''
}`;
if (post.authorFBID || post.authorImageURL) {
const authorImageURL = post.authorFBID
? `https://graph.facebook.com/${
post.authorFBID
}/picture/?height=200&width=200`
: post.authorImageURL;
return (
<div className={className}>
<a href={post.authorURL} target="_blank" rel="noreferrer noopener">
<img src={authorImageURL} alt={post.author} />
</a>
</div>
);
}
return null;
}
renderTitle() {
const post = this.props.post;
return (
<h1 className="postHeaderTitle">
<a
href={`${this.props.config.baseUrl}blog/${utils.getPath(
post.path,
this.props.config.cleanUrl,
)}`}>
{post.title}
</a>
</h1>
);
}
renderPostHeader() {
const post = this.props.post;
const match = post.path.match(/([0-9]+)\/([0-9]+)\/([0-9]+)/);
// Because JavaScript sucks at date handling :(
const year = match[1];
const month = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
][parseInt(match[2], 10) - 1];
const day = parseInt(match[3], 10);
return (
<header className="postHeader">
{this.renderTitle()}
<p className="post-meta">
{month} {day}, {year}
</p>
<div className="authorBlock">
{post.author ? (
<p className="post-authorName">
<a
href={post.authorURL}
target="_blank"
rel="noreferrer noopener">
{post.author}
</a>
{post.authorTitle}
</p>
) : null}
{this.renderAuthorPhoto()}
</div>
</header>
);
}
render() {
return (
<div className="post">
{this.renderPostHeader()}
{this.renderContent()}
</div>
);
}
}
module.exports = BlogPost;

View file

@ -0,0 +1,140 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const classNames = require('classnames');
const React = require('react');
const BlogPost = require('./BlogPost.js');
const BlogSidebar = require('./BlogSidebar.js');
const Container = require('./Container.js');
const Site = require('./Site.js');
const OnPageNav = require('./nav/OnPageNav.js');
const utils = require('./utils.js');
// used for entire blog posts, i.e., each written blog article with sidebar with site header/footer
class BlogPostLayout extends React.Component {
getDescription() {
const descLines = this.props.children.trim().split('\n');
for (let i = 0; i < descLines.length; i++) {
// Don't want blank lines or descriptions that are raw image rendering strings.
if (descLines[i] && !descLines[i].startsWith('![')) {
return descLines[i];
}
}
return null;
}
renderSocialButtons() {
const post = this.props.metadata;
post.path = utils.getPath(post.path, this.props.config.cleanUrl);
const fbComment = this.props.config.facebookAppId &&
this.props.config.facebookComments && (
<div className="blogSocialSectionItem">
{/* Facebook SDK require 'fb-comments' class */}
<div
className="fb-comments"
data-href={`${this.props.config.url +
this.props.config.baseUrl}blog/${post.path}`}
data-width="100%"
data-numposts="5"
data-order-by="time"
/>
</div>
);
const fbLike = this.props.config.facebookAppId && (
<div className="blogSocialSectionItem">
{/* Facebook SDK require 'fb-like' class */}
<div
className="fb-like"
data-href={`${this.props.config.url +
this.props.config.baseUrl}blog/${post.path}`}
data-layout="standard"
data-share="true"
data-width="225"
data-show-faces="false"
/>
</div>
);
const twitterShare = this.props.config.twitter && (
<div className="blogSocialSectionItem">
<a
href="https://twitter.com/share"
className="twitter-share-button"
data-text={post.title}
data-url={`${this.props.config.url + this.props.config.baseUrl}blog/${
post.path
}`}
data-related={this.props.config.twitter}
data-via={post.authorTwitter}
data-show-count="false">
Tweet
</a>
</div>
);
return (
<div className="blogSocialSection">
{twitterShare}
{fbLike}
{fbComment}
</div>
);
}
render() {
const hasOnPageNav = this.props.config.onPageNav === 'separate';
const post = this.props.metadata;
post.path = utils.getPath(post.path, this.props.config.cleanUrl);
const blogSidebarTitleConfig = this.props.config.blogSidebarTitle || {};
return (
<Site
className={classNames('sideNavVisible', {
separateOnPageNav: hasOnPageNav,
})}
url={`blog/${post.path}`}
title={this.props.metadata.title}
language="en"
description={this.getDescription()}
config={this.props.config}
metadata={{blog: true}}>
<div className="docMainWrapper wrapper">
<BlogSidebar
language="en"
current={post}
config={this.props.config}
/>
<Container className="mainContainer postContainer blogContainer">
<div className="lonePost">
<BlogPost
post={post}
content={this.props.children}
language="en"
config={this.props.config}
/>
{this.renderSocialButtons()}
</div>
<div className="blog-recent">
<a className="button" href={`${this.props.config.baseUrl}blog`}>
{blogSidebarTitleConfig.default || 'Recent Posts'}
</a>
</div>
</Container>
{hasOnPageNav && (
<nav className="onPageNav">
<OnPageNav rawContent={this.props.children} />
</nav>
)}
</div>
</Site>
);
}
}
module.exports = BlogPostLayout;

View file

@ -0,0 +1,58 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const Container = require('./Container.js');
const SideNav = require('./nav/SideNav.js');
const MetadataBlog = require('./MetadataBlog.js');
class BlogSidebar extends React.Component {
render() {
let blogSidebarCount = 5;
const blogSidebarTitleConfig = this.props.config.blogSidebarTitle || {};
let blogSidebarTitle = blogSidebarTitleConfig.default || 'Recent Posts';
if (this.props.config.blogSidebarCount) {
if (this.props.config.blogSidebarCount === 'ALL') {
blogSidebarCount = MetadataBlog.length;
blogSidebarTitle = blogSidebarTitleConfig.all || 'All Blog Posts';
} else {
blogSidebarCount = this.props.config.blogSidebarCount;
}
}
const contents = [
{
type: 'CATEGORY',
title: blogSidebarTitle,
children: MetadataBlog.slice(0, blogSidebarCount).map(item => ({
type: 'LINK',
item,
})),
},
];
const title = this.props.current && this.props.current.title;
const current = {
id: title || '',
category: blogSidebarTitle,
};
return (
<Container className="docsNavContainer" id="docsNav" wrapper={false}>
<SideNav
language={this.props.language}
root={`${this.props.config.baseUrl}blog/`}
title="Blog"
contents={contents}
current={current}
/>
</Container>
);
}
}
module.exports = BlogSidebar;

View file

@ -0,0 +1,66 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import _ from 'lodash';
const React = require('react');
const Remarkable = require('./Remarkable');
/**
* The MarkdownBlock component is used to parse markdown and render to HTML.
*/
class MarkdownBlock extends React.Component {
render() {
const groupId = _.uniqueId();
const tabs = this.props.children.map(({title, content}) => ({
id: _.uniqueId(),
groupId,
label: title,
lang: title,
panelContent: <Remarkable source={content} />,
}));
return (
<div className="tabs">
<div className="nav-tabs">
{tabs.map((t, i) => {
const tabId = `tab-group-${groupId}-tab-${t.id}`;
const contentId = `tab-group-${groupId}-content-${t.id}`;
return (
<div
id={tabId}
key={tabId}
className={`nav-link${i === 0 ? ' active' : ''}`}
data-group={`group_${t.groupId}`}
data-tab={contentId}>
{t.label}
</div>
);
})}
</div>
<div className="tab-content">
{tabs.map((t, i) => {
const id = `tab-group-${groupId}-content-${t.id}`;
return (
<div
id={id}
key={id}
className={`tab-pane${i === 0 ? ' active' : ''}`}
data-group={`group_${t.groupId}`}
tabIndex="-1">
{t.panelContent}
</div>
);
})}
</div>
</div>
);
}
}
module.exports = MarkdownBlock;

View file

@ -0,0 +1,17 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const MarkdownBlock = require('./MarkdownBlock.js');
const Container = require('./Container.js');
const GridBlock = require('./GridBlock.js');
// A collection of components to provide to users
module.exports = {
MarkdownBlock,
Container,
GridBlock,
};

View file

@ -0,0 +1,44 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const classNames = require('classnames');
class Container extends React.Component {
render() {
const containerClasses = classNames('container', this.props.className, {
darkBackground: this.props.background === 'dark',
highlightBackground: this.props.background === 'highlight',
lightBackground: this.props.background === 'light',
paddingAll: this.props.padding.indexOf('all') >= 0,
paddingBottom: this.props.padding.indexOf('bottom') >= 0,
paddingLeft: this.props.padding.indexOf('left') >= 0,
paddingRight: this.props.padding.indexOf('right') >= 0,
paddingTop: this.props.padding.indexOf('top') >= 0,
});
let wrappedChildren;
if (this.props.wrapper) {
wrappedChildren = <div className="wrapper">{this.props.children}</div>;
} else {
wrappedChildren = this.props.children;
}
return (
<div className={containerClasses} id={this.props.id}>
{wrappedChildren}
</div>
);
}
}
Container.defaultProps = {
background: null,
padding: [],
wrapper: true,
};
module.exports = Container;

View file

@ -0,0 +1,143 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const {renderToStaticMarkup} = require('react-dom/server');
const MarkdownBlock = require('./MarkdownBlock.js');
const CodeTabsMarkdownBlock = require('./CodeTabsMarkdownBlock.js');
const translate = require('../server/translate.js').translate;
const editThisDoc = translate(
'Edit this Doc|recruitment message asking to edit the doc source',
);
const translateThisDoc = translate(
'Translate this Doc|recruitment message asking to translate the docs',
);
const splitTabsToTitleAndContent = content => {
const titles = content.match(/<!--(.*?)-->/gms);
const tabs = content.split(/<!--.*?-->/gms);
if (!titles || !tabs || !titles.length || !tabs.length) {
return [];
}
tabs.shift();
return titles.map((title, idx) => ({
title: title.substring(4, title.length - 3).trim(),
content: tabs[idx],
}));
};
const cleanTheCodeTag = content => {
const contents = content.split(/(<pre>)(.*?)(<\/pre>)/gms);
let inCodeBlock = false;
const cleanContents = contents.map(c => {
if (c === '<pre>') {
inCodeBlock = true;
return c;
}
if (c === '</pre>') {
inCodeBlock = false;
return c;
}
if (inCodeBlock) {
return c.replace(/\n/g, '<br />');
}
return c;
});
return cleanContents.join('');
};
// inner doc component for article itself
class Doc extends React.Component {
renderContent() {
const {content} = this.props;
let inCodeTabs = false;
const contents = content.split(
/(<!--DOCUSAURUS_CODE_TABS-->\n)(.*?)(\n<!--END_DOCUSAURUS_CODE_TABS-->)/gms,
);
const renderResult = contents.map(c => {
if (c === '<!--DOCUSAURUS_CODE_TABS-->\n') {
inCodeTabs = true;
return '';
}
if (c === '\n<!--END_DOCUSAURUS_CODE_TABS-->') {
inCodeTabs = false;
return '';
}
if (inCodeTabs) {
const codeTabsMarkdownBlock = renderToStaticMarkup(
<CodeTabsMarkdownBlock>
{splitTabsToTitleAndContent(c)}
</CodeTabsMarkdownBlock>,
);
return cleanTheCodeTag(codeTabsMarkdownBlock);
}
return c;
});
return renderResult.join('');
}
render() {
let docSource = this.props.source;
if (this.props.version && this.props.version !== 'next') {
// If versioning is enabled and the current version is not next, we need to trim out "version-*" from the source if we want a valid edit link.
docSource = docSource.match(new RegExp(/version-.*?\/(.*\.md)/, 'i'))[1];
}
const editUrl =
this.props.metadata.custom_edit_url ||
(this.props.config.editUrl && this.props.config.editUrl + docSource);
let editLink = editUrl && (
<a
className="edit-page-link button"
href={editUrl}
target="_blank"
rel="noreferrer noopener">
{editThisDoc}
</a>
);
// If internationalization is enabled, show Recruiting link instead of Edit Link.
if (
this.props.language &&
this.props.language !== 'en' &&
this.props.config.translationRecruitingLink
) {
editLink = (
<a
className="edit-page-link button"
href={`${this.props.config.translationRecruitingLink}/${
this.props.language
}`}
target="_blank"
rel="noreferrer noopener">
{translateThisDoc}
</a>
);
}
return (
<div className="post">
<header className="postHeader">
{editLink}
{!this.props.hideTitle && (
<h1 className="postHeaderTitle">{this.props.title}</h1>
)}
</header>
<article>
<MarkdownBlock>{this.renderContent()}</MarkdownBlock>
</article>
</div>
);
}
}
module.exports = Doc;

View file

@ -0,0 +1,165 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const classNames = require('classnames');
const path = require('path');
const React = require('react');
const url = require('url');
const Container = require('./Container.js');
const Doc = require('./Doc.js');
const DocsSidebar = require('./DocsSidebar.js');
const OnPageNav = require('./nav/OnPageNav.js');
const renderMarkdown = require('./renderMarkdown');
const Site = require('./Site.js');
const translation = require('../server/translation.js');
const docs = require('../server/docs.js');
const {idx, getGitLastUpdatedTime, getGitLastUpdatedBy} = require('./utils.js');
// component used to generate whole webpage for docs, including sidebar/header/footer
class DocsLayout extends React.Component {
getRelativeURL = (from, to) => {
const extension = this.props.config.cleanUrl ? '' : '.html';
const relativeHref =
path
.relative(from, to)
.replace('\\', '/')
.replace(/^\.\.\//, '') + extension;
return url.resolve(
`${this.props.config.baseUrl}${this.props.metadata.permalink}`,
relativeHref,
);
};
render() {
const metadata = this.props.metadata;
const content = this.props.children;
const i18n = translation[metadata.language];
const id = metadata.localized_id;
const defaultTitle = metadata.title;
let DocComponent = Doc;
if (this.props.Doc) {
DocComponent = this.props.Doc;
}
const filepath = docs.getFilePath(metadata);
const updateTime = this.props.config.enableUpdateTime
? getGitLastUpdatedTime(filepath)
: null;
const updateAuthor = this.props.config.enableUpdateBy
? getGitLastUpdatedBy(filepath)
: null;
const title =
idx(i18n, ['localized-strings', 'docs', id, 'title']) || defaultTitle;
const hasOnPageNav = this.props.config.onPageNav === 'separate';
const previousTitle =
idx(i18n, [
'localized-strings',
'docs',
metadata.previous_id,
'sidebar_label',
]) ||
idx(i18n, ['localized-strings', 'docs', metadata.previous_id, 'title']) ||
idx(i18n, ['localized-strings', 'previous']) ||
metadata.previous_title ||
'Previous';
const nextTitle =
idx(i18n, [
'localized-strings',
'docs',
metadata.next_id,
'sidebar_label',
]) ||
idx(i18n, ['localized-strings', 'docs', metadata.next_id, 'title']) ||
idx(i18n, ['localized-strings', 'next']) ||
metadata.next_title ||
'Next';
return (
<Site
config={this.props.config}
className={classNames('sideNavVisible', {
separateOnPageNav: hasOnPageNav,
})}
title={title}
description={renderMarkdown(content.trim().split('\n')[0])}
language={metadata.language}
version={metadata.version}
metadata={metadata}>
<div className="docMainWrapper wrapper">
<DocsSidebar metadata={metadata} />
<Container className="mainContainer">
<DocComponent
metadata={metadata}
content={content}
config={this.props.config}
source={metadata.source}
hideTitle={metadata.hide_title}
title={title}
version={metadata.version}
language={metadata.language}
/>
{(updateTime || updateAuthor) && (
<div className="docLastUpdate">
<em>
Last updated
{updateTime && ` on ${updateTime}`}
{updateAuthor && ` by ${updateAuthor}`}
</em>
</div>
)}
<div className="docs-prevnext">
{metadata.previous_id && (
<a
className="docs-prev button"
href={this.getRelativeURL(
metadata.localized_id,
metadata.previous_id,
)}>
<span className="arrow-prev"> </span>
<span
className={
previousTitle.match(/[a-z][A-Z]/) &&
'function-name-prevnext'
}>
{previousTitle}
</span>
</a>
)}
{metadata.next_id && (
<a
className="docs-next button"
href={this.getRelativeURL(
metadata.localized_id,
metadata.next_id,
)}>
<span
className={
nextTitle.match(/[a-z][A-Z]/) && 'function-name-prevnext'
}>
{nextTitle}
</span>
<span className="arrow-next"> </span>
</a>
)}
</div>
</Container>
{hasOnPageNav && (
<nav className="onPageNav">
<OnPageNav rawContent={content} />
</nav>
)}
</div>
</Site>
);
}
}
module.exports = DocsLayout;

View file

@ -0,0 +1,53 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const fs = require('fs');
const Container = require('./Container.js');
const SideNav = require('./nav/SideNav.js');
const Metadata = require('../core/metadata.js');
const readCategories = require('../server/readCategories.js');
let languages;
const CWD = process.cwd();
if (fs.existsSync(`${CWD}/languages.js`)) {
languages = require(`${CWD}/languages.js`);
} else {
languages = [
{
enabled: true,
name: 'English',
tag: 'en',
},
];
}
class DocsSidebar extends React.Component {
render() {
const {category, sidebar} = this.props.metadata;
const docsCategories = readCategories(sidebar, Metadata, languages);
if (!category) {
return null;
}
return (
<Container className="docsNavContainer" id="docsNav" wrapper={false}>
<SideNav
language={this.props.metadata.language}
root={this.props.root}
title={this.props.title}
contents={docsCategories[this.props.metadata.language]}
current={this.props.metadata}
/>
</Container>
);
}
}
module.exports = DocsSidebar;

View file

@ -0,0 +1,104 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const classNames = require('classnames');
const MarkdownBlock = require('./MarkdownBlock.js');
class GridBlock extends React.Component {
renderBlock(origBlock) {
const blockDefaults = {
imageAlign: 'left',
};
const block = {
...blockDefaults,
...origBlock,
};
const blockClasses = classNames('blockElement', this.props.className, {
alignCenter: this.props.align === 'center',
alignRight: this.props.align === 'right',
fourByGridBlock: this.props.layout === 'fourColumn',
imageAlignSide:
block.image &&
(block.imageAlign === 'left' || block.imageAlign === 'right'),
imageAlignTop: block.image && block.imageAlign === 'top',
imageAlignRight: block.image && block.imageAlign === 'right',
imageAlignBottom: block.image && block.imageAlign === 'bottom',
imageAlignLeft: block.image && block.imageAlign === 'left',
threeByGridBlock: this.props.layout === 'threeColumn',
twoByGridBlock: this.props.layout === 'twoColumn',
});
const topLeftImage =
(block.imageAlign === 'top' || block.imageAlign === 'left') &&
this.renderBlockImage(block.image, block.imageLink, block.imageAlt);
const bottomRightImage =
(block.imageAlign === 'bottom' || block.imageAlign === 'right') &&
this.renderBlockImage(block.image, block.imageLink, block.imageAlt);
return (
<div className={blockClasses} key={block.title}>
{topLeftImage}
<div className="blockContent">
{this.renderBlockTitle(block.title)}
<MarkdownBlock>{block.content}</MarkdownBlock>
</div>
{bottomRightImage}
</div>
);
}
renderBlockImage(image, imageLink, imageAlt) {
if (!image) {
return null;
}
return (
<div className="blockImage">
{imageLink ? (
<a href={imageLink}>
<img src={image} alt={imageAlt} />
</a>
) : (
<img src={image} alt={imageAlt} />
)}
</div>
);
}
renderBlockTitle(title) {
if (!title) {
return null;
}
return (
<h2>
<MarkdownBlock>{title}</MarkdownBlock>
</h2>
);
}
render() {
return (
<div className="gridBlock">
{this.props.contents.map(this.renderBlock, this)}
</div>
);
}
}
GridBlock.defaultProps = {
align: 'left',
contents: [],
layout: 'twoColumn',
};
module.exports = GridBlock;

View file

@ -0,0 +1,192 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
// html head for each page
class Head extends React.Component {
render() {
const links = this.props.config.headerLinks;
const hasBlog = links.some(link => link.blog);
const highlight = {
version: '9.12.0',
theme: 'default',
...this.props.config.highlight,
};
// Use user-provided themeUrl if it exists, else construct one from version and theme.
const highlightThemeURL = highlight.themeUrl
? highlight.themeUrl
: `//cdnjs.cloudflare.com/ajax/libs/highlight.js/${
highlight.version
}/styles/${highlight.theme}.min.css`;
// ensure the siteUrl variable ends with a single slash
const siteUrl = `${(
this.props.config.url + this.props.config.baseUrl
).replace(/\/+$/, '')}/`;
return (
<head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<title>{this.props.title}</title>
<meta name="viewport" content="width=device-width" />
<meta name="generator" content="Docusaurus" />
<meta name="description" content={this.props.description} />
{this.props.version && (
<meta name="docsearch:version" content={this.props.version} />
)}
{this.props.language && (
<meta name="docsearch:language" content={this.props.language} />
)}
<meta property="og:title" content={this.props.title} />
<meta property="og:type" content="website" />
<meta property="og:url" content={this.props.url} />
<meta property="og:description" content={this.props.description} />
{this.props.config.ogImage && (
<meta
property="og:image"
content={siteUrl + this.props.config.ogImage}
/>
)}
<meta name="twitter:card" content="summary" />
{this.props.config.twitterImage && (
<meta
name="twitter:image"
content={siteUrl + this.props.config.twitterImage}
/>
)}
{this.props.config.noIndex && <meta name="robots" content="noindex" />}
{this.props.redirect && (
<meta httpEquiv="refresh" content={`0; URL=${this.props.redirect}`} />
)}
{this.props.config.manifest && (
<link rel="manifest" href={siteUrl + this.props.config.manifest} />
)}
<link
rel="shortcut icon"
href={this.props.config.baseUrl + this.props.config.favicon}
/>
{this.props.config.algolia && (
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/docsearch.js/1/docsearch.min.css"
/>
)}
<link rel="stylesheet" href={highlightThemeURL} />
{hasBlog && (
<link
rel="alternate"
type="application/atom+xml"
href={`${siteUrl}blog/atom.xml`}
title={`${this.props.config.title} Blog ATOM Feed`}
/>
)}
{hasBlog && (
<link
rel="alternate"
type="application/rss+xml"
href={`${siteUrl}blog/feed.xml`}
title={`${this.props.config.title} Blog RSS Feed`}
/>
)}
{this.props.config.gaTrackingId && this.props.config.gaGtag && (
<script
async
src={`https://www.googletagmanager.com/gtag/js?id=${
this.props.config.gaTrackingId
}`}
/>
)}
{this.props.config.gaTrackingId && this.props.config.gaGtag && (
<script
dangerouslySetInnerHTML={{
__html: `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', '${this.props.config.gaTrackingId}');
`,
}}
/>
)}
{this.props.config.gaTrackingId && !this.props.config.gaGtag && (
<script
dangerouslySetInnerHTML={{
__html: `
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '${this.props.config.gaTrackingId}', 'auto');
ga('send', 'pageview');
`,
}}
/>
)}
{/* External resources */}
{this.props.config.stylesheets &&
this.props.config.stylesheets.map(source =>
source.href ? (
<link rel="stylesheet" key={source.href} {...source} />
) : (
<link rel="stylesheet" key={source} href={source} />
),
)}
{this.props.config.scripts &&
this.props.config.scripts.map(source =>
source.src ? (
<script type="text/javascript" key={source.src} {...source} />
) : (
<script type="text/javascript" src={source} key={source} />
),
)}
{this.props.config.scrollToTop && (
<script src="https://unpkg.com/vanilla-back-to-top@7.1.14/dist/vanilla-back-to-top.min.js" />
)}
{this.props.config.scrollToTop && (
<script
dangerouslySetInnerHTML={{
__html: `
document.addEventListener('DOMContentLoaded', function() {
addBackToTop(
${JSON.stringify(
Object.assign(
{},
{zIndex: 100},
this.props.config.scrollToTopOptions,
),
)}
)
});
`,
}}
/>
)}
{this.props.config.usePrism && (
<link
rel="stylesheet"
href={`${this.props.config.baseUrl}css/prism.css`}
/>
)}
{/* Site defined code. Keep these at the end to avoid overriding. */}
<link
rel="stylesheet"
href={`${this.props.config.baseUrl}css/main.css`}
/>
<script src={`${this.props.config.baseUrl}js/codetabs.js`} />
</head>
);
}
}
module.exports = Head;

View file

@ -0,0 +1,20 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const Remarkable = require('./Remarkable');
/**
* The MarkdownBlock component is used to parse markdown and render to HTML.
*/
class MarkdownBlock extends React.Component {
render() {
return <Remarkable source={this.props.children} />;
}
}
module.exports = MarkdownBlock;

View file

@ -0,0 +1,56 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const Head = require('./Head.js');
const translation = require('../server/translation.js');
const {idx} = require('./utils.js');
// Component used to provide same head, header, footer, other scripts to all pages
class Redirect extends React.Component {
render() {
const tagline =
idx(translation, [this.props.language, 'localized-strings', 'tagline']) ||
this.props.config.tagline;
const title = this.props.title
? `${this.props.title} · ${this.props.config.title}`
: (!this.props.config.disableTitleTagline &&
`${this.props.config.title} · ${tagline}`) ||
this.props.config.title;
const description = this.props.description || tagline;
const url =
this.props.config.url +
this.props.config.baseUrl +
(this.props.url || 'index.html');
const redirect = this.props.redirect || false;
return (
<html lang="en">
<Head
config={this.props.config}
description={description}
title={title}
url={url}
redirect={redirect}
/>
<body className={this.props.className}>
<script
dangerouslySetInnerHTML={{
__html: `
<!--
window.location.href = "${this.props.redirect}";
// -->
`,
}}
/>
</body>
</html>
);
}
}
module.exports = Redirect;

View file

@ -0,0 +1,44 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const renderMarkdown = require('./renderMarkdown.js');
class Remarkable extends React.Component {
content() {
if (this.props.source) {
return (
<span
dangerouslySetInnerHTML={{
__html: renderMarkdown(this.props.source),
}}
/>
);
}
return React.Children.map(this.props.children, child => {
if (typeof child === 'string') {
return (
<span dangerouslySetInnerHTML={{__html: renderMarkdown(child)}} />
);
}
return child;
});
}
render() {
const Container = this.props.container;
return <Container>{this.content()}</Container>;
}
}
Remarkable.defaultProps = {
container: 'div',
};
module.exports = Remarkable;

View file

@ -0,0 +1,196 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const fs = require('fs');
const classNames = require('classnames');
const HeaderNav = require('./nav/HeaderNav.js');
const Head = require('./Head.js');
const Footer = require(`${process.cwd()}/core/Footer.js`);
const translation = require('../server/translation.js');
const env = require('../server/env.js');
const liveReloadServer = require('../server/liveReloadServer.js');
const {idx, getPath} = require('./utils.js');
const CWD = process.cwd();
// Component used to provide same head, header, footer, other scripts to all pages
class Site extends React.Component {
mobileNavHasOneRow(headerLinks) {
const hasLanguageDropdown =
env.translation.enabled && env.translation.enabledLanguages().length > 1;
const hasOrdinaryHeaderLinks = headerLinks.some(
link => !(link.languages || link.search),
);
return !(hasLanguageDropdown || hasOrdinaryHeaderLinks);
}
render() {
const tagline =
idx(translation, [this.props.language, 'localized-strings', 'tagline']) ||
this.props.config.tagline;
const title = this.props.title
? `${this.props.title} · ${this.props.config.title}`
: (!this.props.config.disableTitleTagline &&
`${this.props.config.title} · ${tagline}`) ||
this.props.config.title;
const description = this.props.description || tagline;
const path = getPath(
this.props.config.baseUrl + (this.props.url || 'index.html'),
this.props.config.cleanUrl,
);
const url = this.props.config.url + path;
let docsVersion = this.props.version;
const liveReloadScriptUrl = liveReloadServer.getReloadScriptUrl();
if (!docsVersion && fs.existsSync(`${CWD}/versions.json`)) {
const latestVersion = require(`${CWD}/versions.json`)[0];
docsVersion = latestVersion;
}
const navPusherClasses = classNames('navPusher', {
singleRowMobileNav: this.mobileNavHasOneRow(
this.props.config.headerLinks,
),
});
return (
<html lang={this.props.language}>
<Head
config={this.props.config}
description={description}
title={title}
url={url}
language={this.props.language}
version={this.props.version}
/>
<body className={this.props.className}>
<HeaderNav
config={this.props.config}
baseUrl={this.props.config.baseUrl}
title={this.props.config.title}
language={this.props.language}
version={this.props.version}
current={this.props.metadata}
/>
<div className={navPusherClasses}>
{this.props.children}
<Footer config={this.props.config} language={this.props.language} />
</div>
{this.props.config.algolia && (
<script
type="text/javascript"
src="https://cdn.jsdelivr.net/docsearch.js/1/docsearch.min.js"
/>
)}
{this.props.config.facebookAppId && (
<script
dangerouslySetInnerHTML={{
__html: `window.fbAsyncInit = function() {FB.init({appId:'${
this.props.config.facebookAppId
}',xfbml:true,version:'v2.7'});};(function(d, s, id){var js, fjs = d.getElementsByTagName(s)[0];if (d.getElementById(id)) {return;}js = d.createElement(s); js.id = id;js.src = '//connect.facebook.net/en_US/sdk.js';fjs.parentNode.insertBefore(js, fjs);}(document, 'script','facebook-jssdk'));
`,
}}
/>
)}
{this.props.config.facebookPixelId && (
<script
dangerouslySetInnerHTML={{
__html: `
!function(f,b,e,v,n,t,s)
{if(f.fbq)return;n=f.fbq=function(){n.callMethod?
n.callMethod.apply(n,arguments):n.queue.push(arguments)};
if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
n.queue=[];t=b.createElement(e);t.async=!0;
t.src=v;s=b.getElementsByTagName(e)[0];
s.parentNode.insertBefore(t,s)}(window, document,'script',
'https://connect.facebook.net/en_US/fbevents.js');
fbq('init', '${this.props.config.facebookPixelId}');
fbq('track', 'PageView');
`,
}}
/>
)}
{(this.props.config.twitter || this.props.config.twitterUsername) && (
<script
dangerouslySetInnerHTML={{
__html: `window.twttr=(function(d,s, id){var js,fjs=d.getElementsByTagName(s)[0],t=window.twttr||{};if(d.getElementById(id))return t;js=d.createElement(s);js.id=id;js.src='https://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js, fjs);t._e = [];t.ready = function(f) {t._e.push(f);};return t;}(document, 'script', 'twitter-wjs'));`,
}}
/>
)}
{this.props.config.algolia && (
<script
dangerouslySetInnerHTML={{
__html: `
document.addEventListener('keyup', function(e) {
if (e.target !== document.body) {
return;
}
// keyCode for '/' (slash)
if (e.keyCode === 191) {
const search = document.getElementById('search_input_react');
search && search.focus();
}
});
`,
}}
/>
)}
{this.props.config.algolia &&
(this.props.config.algolia.algoliaOptions ? (
<script
dangerouslySetInnerHTML={{
__html: `
var search = docsearch({
${
this.props.config.algolia.appId
? `appId: '${this.props.config.algolia.appId}',`
: ''
}
apiKey: '${this.props.config.algolia.apiKey}',
indexName: '${this.props.config.algolia.indexName}',
inputSelector: '#search_input_react',
algoliaOptions: ${JSON.stringify(
this.props.config.algolia.algoliaOptions,
)
.replace('VERSION', docsVersion)
.replace('LANGUAGE', this.props.language)}
});
`,
}}
/>
) : (
<script
dangerouslySetInnerHTML={{
__html: `
var search = docsearch({
${
this.props.config.algolia.appId
? `appId: '${this.props.config.algolia.appId}',`
: ''
}
apiKey: '${this.props.config.algolia.apiKey}',
indexName: '${this.props.config.algolia.indexName}',
inputSelector: '#search_input_react'
});
`,
}}
/>
))}
{process.env.NODE_ENV === 'development' && liveReloadScriptUrl && (
<script src={liveReloadScriptUrl} />
)}
</body>
</html>
);
}
}
module.exports = Site;

View file

@ -0,0 +1,15 @@
---
title: Truncation Example
---
All this will be part of the blog post summary.
Even this.
<!--truncate-->
But anything from here on down will not be.
Not this.
Or this.

View file

@ -0,0 +1,13 @@
---
title: Non-truncation Example
---
All this will be part of the blog post summary.
Even this.
And anything from here on down will still be.
And this.
And this too.

View file

@ -0,0 +1,20 @@
## foo
### foo
### foo 1
## foo 1
## foo 2
### foo
#### 4th level headings
All 4th level headings should not be shown by default
## bar
### bar
#### bar
4th level heading should be ignored by default, but is should be always taken
into account, when generating slugs
### `bar`
#### `bar`
## bar
### bar
#### bar
## bar

View file

@ -0,0 +1,28 @@
---
id: pokemon-commands
title: Pokemon Commands
---
## Commands
<AUTOGENERATED_TABLE_OF_CONTENTS>
---
## Reference
### `pokemon-run`
Alias: `run`.
### `pokemon-fight`
Alias: `fight`
### `pokemon-bag`
Alias: `bag`
### `pokemon-rename`
Alias: `rename`

View file

@ -0,0 +1,5 @@
---
title: Don't edit this
---
Do not edit this file

View file

@ -0,0 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Anchors rendering 1`] = `"<h1><a class=\\"anchor\\" aria-hidden=\\"true\\" id=\\"hello-world\\"></a><a href=\\"#hello-world\\" aria-hidden=\\"true\\" class=\\"hash-link\\"><svg class=\\"hash-link-icon\\" aria-hidden=\\"true\\" height=\\"16\\" version=\\"1.1\\" viewBox=\\"0 0 16 16\\" width=\\"16\\"><path fill-rule=\\"evenodd\\" d=\\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\\"></path></svg></a>"`;
exports[`Anchors rendering 2`] = `"<h2><a class=\\"anchor\\" aria-hidden=\\"true\\" id=\\"hello-small-world\\"></a><a href=\\"#hello-small-world\\" aria-hidden=\\"true\\" class=\\"hash-link\\"><svg class=\\"hash-link-icon\\" aria-hidden=\\"true\\" height=\\"16\\" version=\\"1.1\\" viewBox=\\"0 0 16 16\\" width=\\"16\\"><path fill-rule=\\"evenodd\\" d=\\"M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z\\"></path></svg></a>"`;

View file

@ -0,0 +1,241 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getTOC with custom heading levels 1`] = `
Array [
Object {
"children": Array [
Object {
"children": Array [],
"content": "foo",
"hashLink": "foo-1",
"rawContent": "foo",
},
Object {
"children": Array [],
"content": "foo 1",
"hashLink": "foo-1-1",
"rawContent": "foo 1",
},
],
"content": "foo",
"hashLink": "foo",
"rawContent": "foo",
},
Object {
"children": Array [],
"content": "foo 1",
"hashLink": "foo-1-2",
"rawContent": "foo 1",
},
Object {
"children": Array [
Object {
"children": Array [],
"content": "foo",
"hashLink": "foo-3",
"rawContent": "foo",
},
Object {
"children": Array [],
"content": "4th level headings",
"hashLink": "4th-level-headings",
"rawContent": "4th level headings",
},
],
"content": "foo 2",
"hashLink": "foo-2",
"rawContent": "foo 2",
},
Object {
"children": Array [
Object {
"children": Array [],
"content": "bar",
"hashLink": "bar-1",
"rawContent": "bar",
},
Object {
"children": Array [],
"content": "bar",
"hashLink": "bar-2",
"rawContent": "bar",
},
Object {
"children": Array [],
"content": "<code>bar</code>",
"hashLink": "bar-3",
"rawContent": "\`bar\`",
},
Object {
"children": Array [],
"content": "<code>bar</code>",
"hashLink": "bar-4",
"rawContent": "\`bar\`",
},
],
"content": "bar",
"hashLink": "bar",
"rawContent": "bar",
},
Object {
"children": Array [
Object {
"children": Array [],
"content": "bar",
"hashLink": "bar-6",
"rawContent": "bar",
},
Object {
"children": Array [],
"content": "bar",
"hashLink": "bar-7",
"rawContent": "bar",
},
],
"content": "bar",
"hashLink": "bar-5",
"rawContent": "bar",
},
Object {
"children": Array [],
"content": "bar",
"hashLink": "bar-8",
"rawContent": "bar",
},
]
`;
exports[`getTOC with defaults 1`] = `
Array [
Object {
"children": Array [
Object {
"children": Array [],
"content": "foo",
"hashLink": "foo-1",
"rawContent": "foo",
},
Object {
"children": Array [],
"content": "foo 1",
"hashLink": "foo-1-1",
"rawContent": "foo 1",
},
],
"content": "foo",
"hashLink": "foo",
"rawContent": "foo",
},
Object {
"children": Array [],
"content": "foo 1",
"hashLink": "foo-1-2",
"rawContent": "foo 1",
},
Object {
"children": Array [
Object {
"children": Array [],
"content": "foo",
"hashLink": "foo-3",
"rawContent": "foo",
},
],
"content": "foo 2",
"hashLink": "foo-2",
"rawContent": "foo 2",
},
Object {
"children": Array [
Object {
"children": Array [],
"content": "bar",
"hashLink": "bar-1",
"rawContent": "bar",
},
Object {
"children": Array [],
"content": "<code>bar</code>",
"hashLink": "bar-3",
"rawContent": "\`bar\`",
},
],
"content": "bar",
"hashLink": "bar",
"rawContent": "bar",
},
Object {
"children": Array [
Object {
"children": Array [],
"content": "bar",
"hashLink": "bar-6",
"rawContent": "bar",
},
],
"content": "bar",
"hashLink": "bar-5",
"rawContent": "bar",
},
Object {
"children": Array [],
"content": "bar",
"hashLink": "bar-8",
"rawContent": "bar",
},
]
`;
exports[`insertTOC AUTOGENERATED_TABLE_OF_CONTENTS does not exist 1`] = `
"## foo
### foo
### foo 1
## foo 1
## foo 2
### foo
#### 4th level headings
All 4th level headings should not be shown by default
## bar
### bar
#### bar
4th level heading should be ignored by default, but is should be always taken
into account, when generating slugs
### \`bar\`
#### \`bar\`
## bar
### bar
#### bar
## bar
"
`;
exports[`insertTOC AUTOGENERATED_TABLE_OF_CONTENTS exists 1`] = `
"
## Commands
- [\`pokemon-run\`](#pokemon-run)
- [\`pokemon-fight\`](#pokemon-fight)
- [\`pokemon-bag\`](#pokemon-bag)
- [\`pokemon-rename\`](#pokemon-rename)
---
## Reference
### \`pokemon-run\`
Alias: \`run\`.
### \`pokemon-fight\`
Alias: \`fight\`
### \`pokemon-bag\`
Alias: \`bag\`
### \`pokemon-rename\`
Alias: \`rename\`"
`;

View file

@ -0,0 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`utils extractBlogPostBeforeTruncate 1`] = `
"---
title: Truncation Example
---
All this will be part of the blog post summary.
Even this.
"
`;
exports[`utils extractBlogPostBeforeTruncate 2`] = `
"---
title: Non-truncation Example
---
All this will be part of the blog post summary.
Even this.
And anything from here on down will still be.
And this.
And this too.
"
`;

View file

@ -0,0 +1,119 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const rules = require('remarkable/lib/rules');
const anchors = require('../anchors');
const md = {
renderer: {
rules: {
heading_open: rules.heading_open,
},
},
};
anchors(md);
const render = md.renderer.rules.heading_open;
test('Anchors rendering', () => {
expect(
render([{hLevel: 1}, {content: 'Hello world'}], 0, {}, {}),
).toMatchSnapshot();
expect(
render([{hLevel: 2}, {content: 'Hello small world'}], 0, {}, {}),
).toMatchSnapshot();
});
test('Each anchor is unique across rendered document', () => {
const tokens = [
{hLevel: 1},
{content: 'Almost unique heading'},
{hLevel: 1},
{content: 'Almost unique heading'},
{hLevel: 1},
{content: 'Almost unique heading 1'},
{hLevel: 1},
{content: 'Almost unique heading 1'},
{hLevel: 1},
{content: 'Almost unique heading 2'},
{hLevel: 1},
{content: 'Almost unique heading'},
];
const options = {};
const env = {};
expect(render(tokens, 0, options, env)).toContain(
'id="almost-unique-heading"',
);
expect(render(tokens, 2, options, env)).toContain(
'id="almost-unique-heading-1"',
);
expect(render(tokens, 4, options, env)).toContain(
'id="almost-unique-heading-1-1"',
);
expect(render(tokens, 6, options, env)).toContain(
'id="almost-unique-heading-1-2"',
);
expect(render(tokens, 8, options, env)).toContain(
'id="almost-unique-heading-2"',
);
expect(render(tokens, 10, options, env)).toContain(
'id="almost-unique-heading-3"',
);
});
test('Each anchor is unique across rendered document. Case 2', () => {
const tokens = [
{hLevel: 1},
{content: 'foo'},
{hLevel: 1},
{content: 'foo 1'},
{hLevel: 1},
{content: 'foo'},
{hLevel: 1},
{content: 'foo 1'},
];
const options = {};
const env = {};
expect(render(tokens, 0, options, env)).toContain('id="foo"');
expect(render(tokens, 2, options, env)).toContain('id="foo-1"');
expect(render(tokens, 4, options, env)).toContain('id="foo-2"');
expect(render(tokens, 6, options, env)).toContain('id="foo-1-1"');
});
test('Anchor index resets on each render', () => {
const tokens = [
{hLevel: 1},
{content: 'Almost unique heading'},
{hLevel: 1},
{content: 'Almost unique heading'},
];
const options = {};
const env = {};
const env2 = {};
expect(render(tokens, 0, options, env)).toContain(
'id="almost-unique-heading"',
);
expect(render(tokens, 2, options, env)).toContain(
'id="almost-unique-heading-1"',
);
expect(render(tokens, 0, options, env2)).toContain(
'id="almost-unique-heading"',
);
expect(render(tokens, 2, options, env2)).toContain(
'id="almost-unique-heading-1"',
);
});
test('Anchor uses default renderer when empty', () => {
expect(render([{hLevel: 1}, {content: null}], 0, {}, {})).toEqual('<h1>');
expect(render([{hLevel: 2}, {content: ''}], 0, {}, {})).toEqual('<h2>');
});

View file

@ -0,0 +1,36 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const toSlug = require('../toSlug');
[
['Hello world! ', 'hello-world'],
['React 16', 'react-16'],
['Hello. // (world?)! ', 'hello-world'],
['Привет мир! ', 'привет-мир'],
['Über Café.', 'uber-cafe'],
['Someting long ...', 'someting-long-'],
].forEach(([input, output]) => {
test(`toSlug('${input}') -> '${output}'`, () => {
expect(toSlug(input)).toBe(output);
});
});
test('unique slugs if `context` argument passed', () => {
[
['foo', 'foo'],
['foo', 'foo-1'],
['foo 1', 'foo-1-1'],
['foo 1', 'foo-1-2'],
['foo 2', 'foo-2'],
['foo', 'foo-3'],
].reduce((context, [input, output]) => {
expect(toSlug(input, context)).toBe(output);
return context;
}, {});
});

View file

@ -0,0 +1,60 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const path = require('path');
const readFileSync = require('fs').readFileSync;
const {getTOC, insertTOC} = require('../toc');
const {extractMetadata} = require('../../server/metadataUtils');
const getTOCmd = readFileSync(
path.join(__dirname, '__fixtures__', 'getTOC.md'),
'utf8',
);
const insertTOCmd = readFileSync(
path.join(__dirname, '__fixtures__', 'insertTOC.md'),
'utf8',
);
describe('getTOC', () => {
test('with defaults', () => {
const headings = getTOC(getTOCmd);
const headingsJson = JSON.stringify(headings);
expect(headings).toMatchSnapshot();
expect(headingsJson).toContain('bar-8'); // maximum unique bar index is 8
expect(headingsJson).not.toContain('4th level headings');
});
test('with custom heading levels', () => {
const headings = getTOC(getTOCmd, 'h2', ['h3', 'h4']);
const headingsJson = JSON.stringify(headings);
expect(headings).toMatchSnapshot();
expect(headingsJson).toContain('bar-8'); // maximum unique bar index is 8
expect(headingsJson).toContain('4th level headings');
});
});
describe('insertTOC', () => {
test('null or undefined content', () => {
expect(insertTOC(null)).toBeNull();
expect(insertTOC(undefined)).toBeUndefined();
});
test('AUTOGENERATED_TABLE_OF_CONTENTS does not exist', () => {
const rawContent = extractMetadata(getTOCmd).rawContent;
expect(insertTOC(rawContent)).toMatchSnapshot();
expect(insertTOC(rawContent)).toEqual(rawContent);
});
test('AUTOGENERATED_TABLE_OF_CONTENTS exists', () => {
const rawContent = extractMetadata(insertTOCmd).rawContent;
expect(insertTOC(rawContent)).toMatchSnapshot();
expect(insertTOC(rawContent)).not.toEqual(rawContent);
});
});

View file

@ -0,0 +1,176 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const path = require('path');
const fs = require('fs');
const shell = require('shelljs');
const utils = require('../utils');
const blogPostWithTruncateContents = fs.readFileSync(
path.join(__dirname, '__fixtures__', 'blog-post-with-truncate.md'),
'utf8',
);
const blogPostWithoutTruncateContents = fs.readFileSync(
path.join(__dirname, '__fixtures__', 'blog-post-without-truncate.md'),
'utf8',
);
describe('utils', () => {
test('blogPostHasTruncateMarker', () => {
expect(utils.blogPostHasTruncateMarker(blogPostWithTruncateContents)).toBe(
true,
);
expect(
utils.blogPostHasTruncateMarker(blogPostWithoutTruncateContents),
).toBe(false);
});
test('extractBlogPostBeforeTruncate', () => {
expect(
utils.extractBlogPostBeforeTruncate(blogPostWithTruncateContents),
).toMatchSnapshot();
expect(
utils.extractBlogPostBeforeTruncate(blogPostWithoutTruncateContents),
).toMatchSnapshot();
});
test('getPath', () => {
// does not change/transform path
expect(utils.getPath('/en/users.html', false)).toBe('/en/users.html');
expect(utils.getPath('/docs/en/versioning.html', false)).toBe(
'/docs/en/versioning.html',
);
expect(utils.getPath(undefined, false)).toBeUndefined();
expect(utils.getPath(null, false)).toBeNull();
// transform to pretty/clean path
const cleanPath = pathStr => utils.getPath(pathStr, true);
expect(cleanPath('/en/users')).toBe('/en/users');
expect(cleanPath('/docs/versioning.html')).toBe('/docs/versioning');
expect(cleanPath('/en/users.html')).toBe('/en/users');
expect(cleanPath('/docs/en/asd/index.html')).toBe('/docs/en/asd/');
expect(cleanPath('/en/help/index.html')).toBe('/en/help/');
expect(cleanPath('/index.html')).toBe('/');
expect(cleanPath('/react/index.html')).toBe('/react/');
expect(cleanPath('/en/help.a.b.c.d.e.html')).toBe('/en/help.a.b.c.d.e');
expect(cleanPath('/en/help.js')).toBe('/en/help.js');
expect(cleanPath('/test.md')).toBe('/test.md');
expect(cleanPath('/blog/7.0.0')).toBe('/blog/7.0.0');
expect(cleanPath('/test/5.html.2')).toBe('/test/5.html.2');
expect(cleanPath('/docs/en/5.2')).toBe('/docs/en/5.2');
});
test('removeExtension', () => {
expect(utils.removeExtension('/endiliey.html')).toBe('/endiliey');
expect(utils.removeExtension('/a.b/')).toBe('/a.b/');
expect(utils.removeExtension('/a.b/c.png')).toBe('/a.b/c');
expect(utils.removeExtension('/a.b/c.d.e')).toBe('/a.b/c.d');
expect(utils.removeExtension('/docs/test')).toBe('/docs/test');
expect(utils.removeExtension('pages.js')).toBe('pages');
});
test('getGitLastUpdatedTime', () => {
// existing test file in repository with git timestamp
const existingFilePath = path.join(__dirname, '__fixtures__', 'test.md');
const gitLastUpdatedTime = utils.getGitLastUpdatedTime(existingFilePath);
expect(typeof gitLastUpdatedTime).toBe('string');
expect(Date.parse(gitLastUpdatedTime)).not.toBeNaN();
expect(gitLastUpdatedTime).not.toBeNull();
// non existing file
const nonExistingFilePath = path.join(
__dirname,
'__fixtures__',
'.nonExisting',
);
expect(utils.getGitLastUpdatedTime(null)).toBeNull();
expect(utils.getGitLastUpdatedTime(undefined)).toBeNull();
expect(utils.getGitLastUpdatedTime(nonExistingFilePath)).toBeNull();
// temporary created file that has no git timestamp
const tempFilePath = path.join(__dirname, '__fixtures__', '.temp');
fs.writeFileSync(tempFilePath, 'Lorem ipsum :)');
expect(utils.getGitLastUpdatedTime(tempFilePath)).toBeNull();
fs.unlinkSync(tempFilePath);
// test renaming and moving file
const tempFilePath2 = path.join(__dirname, '__fixtures__', '.temp2');
const tempFilePath3 = path.join(
__dirname,
'__fixtures__',
'test',
'.temp3',
);
// create new file
shell.exec = jest.fn(() => ({
stdout:
'1539502055, Yangshun Tay\n' +
'\n' +
' create mode 100644 v1/lib/core/__tests__/__fixtures__/.temp2\n',
}));
const createTime = utils.getGitLastUpdatedTime(tempFilePath2);
expect(typeof createTime).toBe('string');
// rename / move the file
shell.exec = jest.fn(() => ({
stdout:
'1539502056, Joel Marcey\n' +
'\n' +
' rename v1/lib/core/__tests__/__fixtures__/{.temp2 => test/.temp3} (100%)\n' +
'1539502055, Yangshun Tay\n' +
'\n' +
' create mode 100644 v1/lib/core/__tests__/__fixtures__/.temp2\n',
}));
const lastUpdateTime = utils.getGitLastUpdatedTime(tempFilePath3);
// should only consider file content change
expect(lastUpdateTime).toEqual(createTime);
});
test('idx', () => {
const a = {};
const b = {hello: 'world'};
const env = {
translation: {
enabled: true,
enabledLanguages: [
{
enabled: true,
name: 'English',
tag: 'en',
},
{
enabled: true,
name: '日本語',
tag: 'ja',
},
],
},
versioning: {
enabled: false,
versions: [],
},
};
const variable = 'enabledLanguages';
expect(utils.idx(a, [('b', 'c')])).toBeUndefined();
expect(utils.idx(b, ['hello'])).toEqual('world');
expect(utils.idx(b, 'hello')).toEqual('world');
expect(utils.idx(env, 'typo')).toBeUndefined();
expect(utils.idx(env, 'versioning')).toEqual({
enabled: false,
versions: [],
});
expect(utils.idx(env, ['translation', 'enabled'])).toEqual(true);
expect(
utils.idx(env, ['translation', variable]).map(lang => lang.tag),
).toEqual(['en', 'ja']);
expect(utils.idx(undefined)).toBeUndefined();
expect(utils.idx(null)).toBeNull();
});
});

View file

@ -0,0 +1,31 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const toSlug = require('./toSlug.js');
/**
* The anchors plugin adds GFM-style anchors to headings.
*/
function anchors(md) {
const originalRender = md.renderer.rules.heading_open;
md.renderer.rules.heading_open = function(tokens, idx, options, env) {
const textToken = tokens[idx + 1];
if (textToken.content) {
const anchor = toSlug(textToken.content, env);
return `<h${
tokens[idx].hLevel
}><a class="anchor" aria-hidden="true" id="${anchor}"></a><a href="#${anchor}" aria-hidden="true" class="hash-link"><svg class="hash-link-icon" aria-hidden="true" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>`;
}
return originalRender(tokens, idx, options, env);
};
}
module.exports = anchors;

View file

@ -0,0 +1,339 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const CWD = process.cwd();
const React = require('react');
const fs = require('fs');
const classNames = require('classnames');
const loadConfig = require('../../server/config');
const siteConfig = loadConfig(`${CWD}/siteConfig.js`);
const translation = require('../../server/translation.js');
const env = require('../../server/env.js');
const translate = require('../../server/translate.js').translate;
const setLanguage = require('../../server/translate.js').setLanguage;
const readMetadata = require('../../server/readMetadata.js');
readMetadata.generateMetadataDocs();
const Metadata = require('../metadata.js');
const {idx, getPath} = require('../utils.js');
const extension = siteConfig.cleanUrl ? '' : '.html';
// language dropdown nav item for when translations are enabled
class LanguageDropDown extends React.Component {
render() {
setLanguage(this.props.language || 'en');
const helpTranslateString = translate(
'Help Translate|recruit community translators for your project',
);
const docsPart = `${siteConfig.docsUrl ? `${siteConfig.docsUrl}/` : ''}`;
// add all enabled languages to dropdown
const enabledLanguages = env.translation
.enabledLanguages()
.filter(lang => lang.tag !== this.props.language)
.map(lang => {
// build the href so that we try to stay in current url but change the language.
let href = siteConfig.baseUrl + lang.tag;
if (
this.props.current &&
this.props.current.permalink &&
this.props.language
) {
href =
siteConfig.baseUrl +
this.props.current.permalink.replace(
new RegExp(`^${docsPart}${this.props.language}/`),
`${docsPart}${lang.tag}/`,
);
} else if (this.props.current.id && this.props.current.id !== 'index') {
href = `${siteConfig.baseUrl + lang.tag}/${this.props.current.id}`;
}
return (
<li key={lang.tag}>
<a href={getPath(href, this.props.cleanUrl)}>{lang.name}</a>
</li>
);
});
// if no languages are enabled besides English, return null
if (enabledLanguages.length < 1) {
return null;
}
// Get the current language full name for display in the header nav
const currentLanguage = env.translation
.enabledLanguages()
.filter(lang => lang.tag === this.props.language)
.map(lang => lang.name);
// add Crowdin project recruiting link
if (siteConfig.translationRecruitingLink) {
enabledLanguages.push(
<li key="recruiting">
<a
href={siteConfig.translationRecruitingLink}
target="_blank"
rel="noreferrer noopener">
{helpTranslateString}
</a>
</li>,
);
}
return (
<span>
<li key="languages">
<a id="languages-menu" href="#">
<img
className="languages-icon"
src={`${this.props.baseUrl}img/language.svg`}
alt="Languages icon"
/>
{currentLanguage}
</a>
<div id="languages-dropdown" className="hide">
<ul id="languages-dropdown-items">{enabledLanguages}</ul>
</div>
</li>
<script
dangerouslySetInnerHTML={{
__html: `
const languagesMenuItem = document.getElementById("languages-menu");
const languagesDropDown = document.getElementById("languages-dropdown");
languagesMenuItem.addEventListener("click", function(event) {
event.preventDefault();
if (languagesDropDown.className == "hide") {
languagesDropDown.className = "visible";
} else {
languagesDropDown.className = "hide";
}
});
`,
}}
/>
</span>
);
}
}
// header navbar used by all pages generated with docusaurus
class HeaderNav extends React.Component {
// function to generate each header link, used with each object in siteConfig.headerLinks
makeLinks(link) {
let href;
let docItemActive = false;
let docGroupActive = false;
if (link.search && this.props.config.algolia) {
// return algolia search bar
const placeholder = this.props.config.algolia.placeholder || 'Search';
return (
<li className="navSearchWrapper reactNavSearchWrapper" key="search">
<input
id="search_input_react"
type="text"
placeholder={placeholder}
title={placeholder}
/>
</li>
);
}
if (link.languages) {
if (
env.translation.enabled &&
env.translation.enabledLanguages().length > 1
) {
return (
<LanguageDropDown
baseUrl={this.props.baseUrl}
language={this.props.language}
current={this.props.current}
cleanUrl={this.props.config.cleanUrl}
key="languagedropdown"
/>
);
}
return null;
}
if (link.doc) {
// set link to document with current page's language/version
const langPart = env.translation.enabled
? `${this.props.language || 'en'}-`
: '';
const versionPart =
env.versioning.enabled && this.props.version !== 'next'
? `version-${this.props.version || env.versioning.defaultVersion}-`
: '';
const id = langPart + versionPart + link.doc;
if (!Metadata[id]) {
let errorStr = `Processing the following \`doc\` field in \`headerLinks\` within \`siteConfig.js\`: '${
link.doc
}'`;
if (id === link.doc) {
errorStr +=
' It looks like there is no document with that id that exists in your docs directory. Please double check the spelling of your `doc` field and the `id` fields of your docs.';
} else {
errorStr += `${'. Check the spelling of your `doc` field. If that seems sane, and a document in your docs folder exists with that `id` value, \nthen this is likely a bug in Docusaurus.' +
' Docusaurus thinks one or both of translations (currently set to: '}${
env.translation.enabled
}) or versioning (currently set to: ${
env.versioning.enabled
}) is enabled when maybe they should not be. \nThus my internal id for this doc is: '${id}'. Please file an issue for this possible bug on GitHub.`;
}
throw new Error(errorStr);
}
href =
this.props.config.baseUrl +
getPath(Metadata[id].permalink, this.props.config.cleanUrl);
const {id: currentID, sidebar} = this.props.current;
docItemActive = currentID && currentID === id;
docGroupActive = sidebar && sidebar === Metadata[id].sidebar;
} else if (link.page) {
// set link to page with current page's language if appropriate
const language = this.props.language || '';
if (fs.existsSync(`${CWD}/pages/en/${link.page}.js`)) {
href =
siteConfig.baseUrl +
(env.translation.enabled ? `${language}/` : '') +
link.page +
extension;
} else {
href = siteConfig.baseUrl + link.page + extension;
}
} else if (link.href) {
// set link to specified href
href = link.href;
} else if (link.blog) {
// set link to blog url
href = `${this.props.baseUrl}blog/`;
}
const itemClasses = classNames({
siteNavGroupActive:
(link.doc && docGroupActive) || (link.blog && this.props.current.blog),
siteNavItemActive:
docItemActive ||
(link.blog && this.props.current.blogListing) ||
(link.page && link.page === this.props.current.id),
});
const i18n = translation[this.props.language];
return (
<li key={`${link.label}page`} className={itemClasses}>
<a href={href} target={link.external ? '_blank' : '_self'}>
{idx(i18n, ['localized-strings', 'links', link.label]) || link.label}
</a>
</li>
);
}
renderResponsiveNav() {
const headerLinks = this.props.config.headerLinks;
// add language drop down to end if location not specified
let languages = false;
headerLinks.forEach(link => {
if (link.languages) {
languages = true;
}
});
if (!languages) {
headerLinks.push({languages: true});
}
let search = false;
headerLinks.forEach(link => {
if (
link.doc &&
!fs.existsSync(`${CWD}/../${readMetadata.getDocsPath()}/`)
) {
throw new Error(
`You have 'doc' in your headerLinks, but no '${readMetadata.getDocsPath()}' folder exists one level up from ` +
`'website' folder. Did you run \`docusaurus-init\` or \`npm run examples\`? If so, ` +
`make sure you rename 'docs-examples-from-docusaurus' to 'docs'.`,
);
}
if (link.blog && !fs.existsSync(`${CWD}/blog/`)) {
throw new Error(
"You have 'blog' in your headerLinks, but no 'blog' folder exists in your " +
"'website' folder. Did you run `docusaurus-init` or `npm run examples`? If so, " +
"make sure you rename 'blog-examples-from-docusaurus' to 'blog'.",
);
}
if (link.page && !fs.existsSync(`${CWD}/pages/`)) {
throw new Error(
"You have 'page' in your headerLinks, but no 'pages' folder exists in your " +
"'website' folder.",
);
}
// We will add search bar to end if location not specified
if (link.search) {
search = true;
}
});
if (!search && this.props.config.algolia) {
headerLinks.push({search: true});
}
return (
<div className="navigationWrapper navigationSlider">
<nav className="slidingNav">
<ul className="nav-site nav-site-internal">
{headerLinks.map(this.makeLinks, this)}
</ul>
</nav>
</div>
);
}
render() {
const headerClass = siteConfig.headerIcon
? 'headerTitleWithLogo'
: 'headerTitle';
const versionsLink =
this.props.baseUrl +
(env.translation.enabled
? `${this.props.language}/versions${extension}`
: `versions${extension}`);
return (
<div className="fixedHeaderContainer">
<div className="headerWrapper wrapper">
<header>
<a
href={
this.props.baseUrl +
(env.translation.enabled ? this.props.language : '')
}>
{siteConfig.headerIcon && (
<img
className="logo"
src={this.props.baseUrl + siteConfig.headerIcon}
alt={siteConfig.title}
/>
)}
{!this.props.config.disableHeaderTitle && (
<h2 className={headerClass}>{this.props.title}</h2>
)}
</a>
{env.versioning.enabled && (
<a href={versionsLink}>
<h3>{this.props.version || env.versioning.defaultVersion}</h3>
</a>
)}
{this.renderResponsiveNav()}
</header>
</div>
</div>
);
}
}
HeaderNav.defaultProps = {
current: {},
};
module.exports = HeaderNav;

View file

@ -0,0 +1,47 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const siteConfig = require(`${process.cwd()}/siteConfig.js`);
const {getTOC} = require('../toc');
const Link = ({hashLink, content}) => (
<a
href={`#${hashLink}`}
dangerouslySetInnerHTML={{
__html: content,
}}
/>
);
const Headings = ({headings}) => {
if (!headings.length) return null;
return (
<ul className="toc-headings">
{headings.map(heading => (
<li key={heading.hashLink}>
<Link hashLink={heading.hashLink} content={heading.content} />
<Headings headings={heading.children} />
</li>
))}
</ul>
);
};
class OnPageNav extends React.Component {
render() {
const customTags = siteConfig.onPageNavHeadings;
const headings = customTags
? getTOC(this.props.rawContent, customTags.topLevel, customTags.sub)
: getTOC(this.props.rawContent);
return <Headings headings={headings} />;
}
}
module.exports = OnPageNav;

View file

@ -0,0 +1,220 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const React = require('react');
const classNames = require('classnames');
const siteConfig = require(`${process.cwd()}/siteConfig.js`);
const translation = require('../../server/translation.js');
const {getPath, idx} = require('../utils.js');
class SideNav extends React.Component {
// return appropriately translated category string
getLocalizedCategoryString(category) {
const categoryString =
idx(translation, [
this.props.language,
'localized-strings',
'categories',
category,
]) || category;
return categoryString;
}
// return appropriately translated label to use for doc/blog in sidebar
getLocalizedString(metadata) {
let localizedString;
const i18n = translation[this.props.language];
const id = metadata.localized_id;
const sbTitle = metadata.sidebar_label;
if (sbTitle) {
localizedString =
idx(i18n, ['localized-strings', 'docs', id, 'sidebar_label']) ||
sbTitle;
} else {
localizedString =
idx(i18n, ['localized-strings', 'docs', id, 'title']) || metadata.title;
}
return localizedString;
}
// return link to doc in sidebar
getLink(metadata) {
if (metadata.permalink) {
const targetLink = getPath(metadata.permalink, siteConfig.cleanUrl);
if (targetLink.match(/^https?:/)) {
return targetLink;
}
return siteConfig.baseUrl + targetLink;
}
if (metadata.path) {
return `${siteConfig.baseUrl}blog/${getPath(
metadata.path,
siteConfig.cleanUrl,
)}`;
}
return null;
}
renderCategory = categoryItem => {
let ulClassName = '';
let categoryClassName = 'navGroupCategoryTitle';
let arrow;
if (siteConfig.docsSideNavCollapsible) {
categoryClassName += ' collapsible';
ulClassName = 'hide';
arrow = (
<span className="arrow">
<svg width="24" height="24" viewBox="0 0 24 24">
<path
fill="#565656"
d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z"
/>
<path d="M0 0h24v24H0z" fill="none" />
</svg>
</span>
);
}
return (
<div className="navGroup" key={categoryItem.title}>
<h3 className={categoryClassName}>
{this.getLocalizedCategoryString(categoryItem.title)}
{arrow}
</h3>
<ul className={ulClassName}>
{categoryItem.children.map(item => {
switch (item.type) {
case 'LINK':
return this.renderItemLink(item);
case 'SUBCATEGORY':
return this.renderSubcategory(item);
default:
return null;
}
})}
</ul>
</div>
);
};
renderSubcategory = subcategoryItem => (
<div className="navGroup subNavGroup" key={subcategoryItem.title}>
<h4 className="navGroupSubcategoryTitle">
{this.getLocalizedCategoryString(subcategoryItem.title)}
</h4>
<ul>{subcategoryItem.children.map(this.renderItemLink)}</ul>
</div>
);
renderItemLink = linkItem => {
const linkMetadata = linkItem.item;
const itemClasses = classNames('navListItem', {
navListItemActive: linkMetadata.id === this.props.current.id,
});
return (
<li className={itemClasses} key={linkMetadata.id}>
<a className="navItem" href={this.getLink(linkMetadata)}>
{this.getLocalizedString(linkMetadata)}
</a>
</li>
);
};
render() {
return (
<nav className="toc">
<div className="toggleNav">
<section className="navWrapper wrapper">
<div className="navBreadcrumb wrapper">
<div className="navToggle" id="navToggler">
<i />
</div>
<h2>
<i></i>
<span>
{this.getLocalizedCategoryString(this.props.current.category)}
</span>
</h2>
{siteConfig.onPageNav === 'separate' && (
<div className="tocToggler" id="tocToggler">
<i className="icon-toc" />
</div>
)}
</div>
<div className="navGroups">
{this.props.contents.map(this.renderCategory)}
</div>
</section>
</div>
<script
dangerouslySetInnerHTML={{
__html: `
var coll = document.getElementsByClassName('collapsible');
var checkActiveCategory = true;
for (var i = 0; i < coll.length; i++) {
var links = coll[i].nextElementSibling.getElementsByTagName('*');
if (checkActiveCategory){
for (var j = 0; j < links.length; j++) {
if (links[j].classList.contains('navListItemActive')){
coll[i].nextElementSibling.classList.toggle('hide');
coll[i].childNodes[1].classList.toggle('rotate');
checkActiveCategory = false;
break;
}
}
}
coll[i].addEventListener('click', function() {
var arrow = this.childNodes[1];
arrow.classList.toggle('rotate');
var content = this.nextElementSibling;
content.classList.toggle('hide');
});
}
document.addEventListener('DOMContentLoaded', function() {
createToggler('#navToggler', '#docsNav', 'docsSliderActive');
createToggler('#tocToggler', 'body', 'tocActive');
const headings = document.querySelector('.toc-headings');
headings && headings.addEventListener('click', function(event) {
if (event.target.tagName === 'A') {
document.body.classList.remove('tocActive');
}
}, false);
function createToggler(togglerSelector, targetSelector, className) {
var toggler = document.querySelector(togglerSelector);
var target = document.querySelector(targetSelector);
if (!toggler) {
return;
}
toggler.onclick = function(event) {
event.preventDefault();
target.classList.toggle(className);
};
}
});
`,
}}
/>
</nav>
);
}
}
SideNav.defaultProps = {
contents: [],
};
module.exports = SideNav;

View file

@ -0,0 +1,123 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const hljs = require('highlight.js');
const Markdown = require('remarkable');
const prismjs = require('prismjs');
const deepmerge = require('deepmerge');
const chalk = require('chalk');
const anchors = require('./anchors.js');
const CWD = process.cwd();
const alias = {
js: 'jsx',
html: 'markup',
sh: 'bash',
md: 'markdown',
};
class MarkdownRenderer {
constructor() {
const siteConfig = require(`${CWD}/siteConfig.js`);
let markdownOptions = {
// Highlight.js expects hljs css classes on the code element.
// This results in <pre><code class="hljs css languages-jsx">
langPrefix: 'hljs css language-',
highlight(str, lang) {
// User's own custom highlighting function
if (siteConfig.highlight && siteConfig.highlight.hljs) {
siteConfig.highlight.hljs(hljs);
}
// Fallback to default language
lang =
lang || (siteConfig.highlight && siteConfig.highlight.defaultLang);
if (lang === 'text') {
return str;
}
if (lang) {
try {
if (
siteConfig.usePrism === true ||
(siteConfig.usePrism &&
siteConfig.usePrism.length > 0 &&
siteConfig.usePrism.indexOf(lang) !== -1)
) {
const language = alias[lang] || lang;
try {
// Currently people using prismjs on Node have to individually require()
// every single language (https://github.com/PrismJS/prism/issues/593)
require(`prismjs/components/prism-${language}.min`);
return prismjs.highlight(str, prismjs.languages[language]);
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
const unsupportedLanguageError = chalk.yellow(
`Warning: ${chalk.red(
language,
)} is not supported by prismjs.` +
'\nPlease refer to https://prismjs.com/#languages-list for the list of supported languages.',
);
console.log(unsupportedLanguageError);
} else console.error(err);
}
}
if (hljs.getLanguage(lang)) {
return hljs.highlight(lang, str).value;
}
} catch (err) {
console.error(err);
}
}
try {
return hljs.highlightAuto(str).value;
} catch (err) {
console.error(err);
}
return '';
},
html: true,
linkify: true,
};
// Allow overriding default options
if (siteConfig.markdownOptions) {
markdownOptions = deepmerge(
{},
markdownOptions,
siteConfig.markdownOptions,
);
}
const md = new Markdown(markdownOptions);
// Register anchors plugin
md.use(anchors);
// Allow client sites to register their own plugins
if (siteConfig.markdownPlugins) {
siteConfig.markdownPlugins.forEach(plugin => {
md.use(plugin);
});
}
this.md = md;
}
toHtml(source) {
const html = this.md.render(source);
// Ensure fenced code blocks use Highlight.js hljs class
// https://github.com/jonschlinkert/remarkable/issues/224
return html.replace(/<pre><code>/g, '<pre><code class="hljs">');
}
}
const renderMarkdown = new MarkdownRenderer();
module.exports = source => renderMarkdown.toHtml(source);

View file

@ -0,0 +1,77 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// ES2015 does not support regexp with unicode categories,
// so we need to list all the unicode ranges manually
// to get analog of [\P{L}\P{N}] from ES2018
// see: https://github.com/danielberndt/babel-plugin-utf-8-regex
const letters =
'\u0041-\u005A\u0061-\u007A\u00AA\u00B5\u00BA\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02C1\u02C6-\u02D1\u02E0-\u02E4\u02EC\u02EE\u0370-\u0374\u0376\u0377\u037A-\u037D\u0386\u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03F5\u03F7-\u0481\u048A-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05D0-\u05EA\u05F0-\u05F2\u0620-\u064A\u066E\u066F\u0671-\u06D3\u06D5\u06E5\u06E6\u06EE\u06EF\u06FA-\u06FC\u06FF\u0710\u0712-\u072F\u074D-\u07A5\u07B1\u07CA-\u07EA\u07F4\u07F5\u07FA\u0800-\u0815\u081A\u0824\u0828\u0840-\u0858\u08A0\u08A2-\u08AC\u0904-\u0939\u093D\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097F\u0985-\u098C\u098F\u0990\u0993-\u09A8\u09AA-\u09B0\u09B2\u09B6-\u09B9\u09BD\u09CE\u09DC\u09DD\u09DF-\u09E1\u09F0\u09F1\u0A05-\u0A0A\u0A0F\u0A10\u0A13-\u0A28\u0A2A-\u0A30\u0A32\u0A33\u0A35\u0A36\u0A38\u0A39\u0A59-\u0A5C\u0A5E\u0A72-\u0A74\u0A85-\u0A8D\u0A8F-\u0A91\u0A93-\u0AA8\u0AAA-\u0AB0\u0AB2\u0AB3\u0AB5-\u0AB9\u0ABD\u0AD0\u0AE0\u0AE1\u0B05-\u0B0C\u0B0F\u0B10\u0B13-\u0B28\u0B2A-\u0B30\u0B32\u0B33\u0B35-\u0B39\u0B3D\u0B5C\u0B5D\u0B5F-\u0B61\u0B71\u0B83\u0B85-\u0B8A\u0B8E-\u0B90\u0B92-\u0B95\u0B99\u0B9A\u0B9C\u0B9E\u0B9F\u0BA3\u0BA4\u0BA8-\u0BAA\u0BAE-\u0BB9\u0BD0\u0C05-\u0C0C\u0C0E-\u0C10\u0C12-\u0C28\u0C2A-\u0C33\u0C35-\u0C39\u0C3D\u0C58\u0C59\u0C60\u0C61\u0C85-\u0C8C\u0C8E-\u0C90\u0C92-\u0CA8\u0CAA-\u0CB3\u0CB5-\u0CB9\u0CBD\u0CDE\u0CE0\u0CE1\u0CF1\u0CF2\u0D05-\u0D0C\u0D0E-\u0D10\u0D12-\u0D3A\u0D3D\u0D4E\u0D60\u0D61\u0D7A-\u0D7F\u0D85-\u0D96\u0D9A-\u0DB1\u0DB3-\u0DBB\u0DBD\u0DC0-\u0DC6\u0E01-\u0E30\u0E32\u0E33\u0E40-\u0E46\u0E81\u0E82\u0E84\u0E87\u0E88\u0E8A\u0E8D\u0E94-\u0E97\u0E99-\u0E9F\u0EA1-\u0EA3\u0EA5\u0EA7\u0EAA\u0EAB\u0EAD-\u0EB0\u0EB2\u0EB3\u0EBD\u0EC0-\u0EC4\u0EC6\u0EDC-\u0EDF\u0F00\u0F40-\u0F47\u0F49-\u0F6C\u0F88-\u0F8C\u1000-\u102A\u103F\u1050-\u1055\u105A-\u105D\u1061\u1065\u1066\u106E-\u1070\u1075-\u1081\u108E\u10A0-\u10C5\u10C7\u10CD\u10D0-\u10FA\u10FC-\u1248\u124A-\u124D\u1250-\u1256\u1258\u125A-\u125D\u1260-\u1288\u128A-\u128D\u1290-\u12B0\u12B2-\u12B5\u12B8-\u12BE\u12C0\u12C2-\u12C5\u12C8-\u12D6\u12D8-\u1310\u1312-\u1315\u1318-\u135A\u1380-\u138F\u13A0-\u13F4\u1401-\u166C\u166F-\u167F\u1681-\u169A\u16A0-\u16EA\u1700-\u170C\u170E-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176C\u176E-\u1770\u1780-\u17B3\u17D7\u17DC\u1820-\u1877\u1880-\u18A8\u18AA\u18B0-\u18F5\u1900-\u191C\u1950-\u196D\u1970-\u1974\u1980-\u19AB\u19C1-\u19C7\u1A00-\u1A16\u1A20-\u1A54\u1AA7\u1B05-\u1B33\u1B45-\u1B4B\u1B83-\u1BA0\u1BAE\u1BAF\u1BBA-\u1BE5\u1C00-\u1C23\u1C4D-\u1C4F\u1C5A-\u1C7D\u1CE9-\u1CEC\u1CEE-\u1CF1\u1CF5\u1CF6\u1D00-\u1DBF\u1E00-\u1F15\u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D\u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBE\u1FC2-\u1FC4\u1FC6-\u1FCC\u1FD0-\u1FD3\u1FD6-\u1FDB\u1FE0-\u1FEC\u1FF2-\u1FF4\u1FF6-\u1FFC\u2071\u207F\u2090-\u209C\u2102\u2107\u210A-\u2113\u2115\u2119-\u211D\u2124\u2126\u2128\u212A-\u212D\u212F-\u2139\u213C-\u213F\u2145-\u2149\u214E\u2183\u2184\u2C00-\u2C2E\u2C30-\u2C5E\u2C60-\u2CE4\u2CEB-\u2CEE\u2CF2\u2CF3\u2D00-\u2D25\u2D27\u2D2D\u2D30-\u2D67\u2D6F\u2D80-\u2D96\u2DA0-\u2DA6\u2DA8-\u2DAE\u2DB0-\u2DB6\u2DB8-\u2DBE\u2DC0-\u2DC6\u2DC8-\u2DCE\u2DD0-\u2DD6\u2DD8-\u2DDE\u2E2F\u3005\u3006\u3031-\u3035\u303B\u303C\u3041-\u3096\u309D-\u309F\u30A1-\u30FA\u30FC-\u30FF\u3105-\u312D\u3131-\u318E\u31A0-\u31BA\u31F0-\u31FF\u3400-\u4DB5\u4E00-\u9FCC\uA000-\uA48C\uA4D0-\uA4FD\uA500-\uA60C\uA610-\uA61F\uA62A\uA62B\uA640-\uA66E\uA67F-\uA697\uA6A0-\uA6E5\uA717-\uA71F\uA722-\uA788\uA78B-\uA78E\uA790-\uA793\uA7A0-\uA7AA\uA7F8-\uA801\uA803-\uA805\uA807-\uA80A\uA80C-\uA822\uA840-\uA873\uA882-\uA8B3\uA8F2-\uA8F7\uA8FB\uA90A-\uA925\uA930-\uA946\uA960-\uA97C\uA984-\uA9B2\uA9CF\uAA00-\uAA28\uAA40-\uAA42\uAA44-\uAA4B\uAA60-\uAA76\uAA7A\uAA80-\uAAAF\uAAB1\uAAB5\uAAB6\uAAB9-\uAABD\uAAC0\uAAC2\uAADB-\uAADD\uAAE0-\uAAEA\uAAF2-\uAAF4\uAB01-\uAB06\uAB09-\uAB0E\uAB11-\uAB16\uAB20-\uAB26\uAB28-\uAB2E\uABC0-\uABE2\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFA6D\uFA70-\uFAD9\uFB00-\uFB06\uFB13-\uFB17\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB38-\uFB3C\uFB3E\uFB40\uFB41\uFB43\uFB44\uFB46-\uFBB1\uFBD3-\uFD3D\uFD50-\uFD8F\uFD92-\uFDC7\uFDF0-\uFDFB\uFE70-\uFE74\uFE76-\uFEFC\uFF21-\uFF3A\uFF41-\uFF5A\uFF66-\uFFBE\uFFC2-\uFFC7\uFFCA-\uFFCF\uFFD2-\uFFD7\uFFDA-\uFFDC';
const numbers =
'\u0030-\u0039\u00B2\u00B3\u00B9\u00BC-\u00BE\u0660-\u0669\u06F0-\u06F9\u07C0-\u07C9\u0966-\u096F\u09E6-\u09EF\u09F4-\u09F9\u0A66-\u0A6F\u0AE6-\u0AEF\u0B66-\u0B6F\u0B72-\u0B77\u0BE6-\u0BF2\u0C66-\u0C6F\u0C78-\u0C7E\u0CE6-\u0CEF\u0D66-\u0D75\u0E50-\u0E59\u0ED0-\u0ED9\u0F20-\u0F33\u1040-\u1049\u1090-\u1099\u1369-\u137C\u16EE-\u16F0\u17E0-\u17E9\u17F0-\u17F9\u1810-\u1819\u1946-\u194F\u19D0-\u19DA\u1A80-\u1A89\u1A90-\u1A99\u1B50-\u1B59\u1BB0-\u1BB9\u1C40-\u1C49\u1C50-\u1C59\u2070\u2074-\u2079\u2080-\u2089\u2150-\u2182\u2185-\u2189\u2460-\u249B\u24EA-\u24FF\u2776-\u2793\u2CFD\u3007\u3021-\u3029\u3038-\u303A\u3192-\u3195\u3220-\u3229\u3248-\u324F\u3251-\u325F\u3280-\u3289\u32B1-\u32BF\uA620-\uA629\uA6E6-\uA6EF\uA830-\uA835\uA8D0-\uA8D9\uA900-\uA909\uA9D0-\uA9D9\uAA50-\uAA59\uABF0-\uABF9\uFF10-\uFF19';
const exceptAlphanum = new RegExp(`[^${[letters, numbers].join('')}]`, 'g');
/**
* Converts a string to a slug, that can be used in heading anchors
*
* @param {string} string
* @param {Object} [context={}] - an optional context to track used slugs and
* ensure that new slug will be unique
*
* @return {string}
*/
module.exports = (string, context = {}) => {
// var accents = "àáäâèéëêìíïîòóöôùúüûñç";
const accents =
'\u00e0\u00e1\u00e4\u00e2\u00e8' +
'\u00e9\u00eb\u00ea\u00ec\u00ed\u00ef' +
'\u00ee\u00f2\u00f3\u00f6\u00f4\u00f9' +
'\u00fa\u00fc\u00fb\u00f1\u00e7';
const without = 'aaaaeeeeiiiioooouuuunc';
let slug = string
.toString()
// Handle uppercase characters
.toLowerCase()
// Handle accentuated characters
.replace(new RegExp(`[${accents}]`, 'g'), c =>
without.charAt(accents.indexOf(c)),
)
// Replace `.`, `(` and `?` with blank string like Github does
.replace(/\.|\(|\?/g, '')
// Dash special characters
.replace(exceptAlphanum, '-')
// Compress multiple dash
.replace(/-+/g, '-')
// Trim dashes
.replace(/^-|-$/g, '');
// Add trailing `-` if string contains ` ...` in the end like Github does
if (/\s[.]{1,}/.test(string)) {
slug += '-';
}
if (!context.slugStats) {
context.slugStats = {};
}
if (typeof context.slugStats[slug] === 'number') {
// search for an index, that will not clash with an existing headings
while (
typeof context.slugStats[`${slug}-${++context.slugStats[slug]}`] ===
'number'
);
slug += `-${context.slugStats[slug]}`;
}
// we are tracking both original anchors and suffixed to avoid future name
// clashing with headings with numbers e.g. `#Foo 1` may clash with the second `#Foo`
context.slugStats[slug] = 0;
return slug;
};

View file

@ -0,0 +1,76 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const Remarkable = require('remarkable');
const mdToc = require('markdown-toc');
const toSlug = require('./toSlug');
const tocRegex = new RegExp('<AUTOGENERATED_TABLE_OF_CONTENTS>', 'i');
/**
* Returns a table of content from the headings
*
* @return array
* Array of heading objects with `hashLink`, `content` and `children` fields
*
*/
function getTOC(content, headingTags = 'h2', subHeadingTags = 'h3') {
const tagToLevel = tag => Number(tag.slice(1));
const headingLevels = [].concat(headingTags).map(tagToLevel);
const subHeadingLevels = subHeadingTags
? [].concat(subHeadingTags).map(tagToLevel)
: [];
const allowedHeadingLevels = headingLevels.concat(subHeadingLevels);
const md = new Remarkable();
const headings = mdToc(content).json;
const toc = [];
const context = {};
let current;
headings.forEach(heading => {
// we need always generate slugs to ensure, that we will have consistent
// slug indexes for headings with the same names
const hashLink = toSlug(heading.content, context);
if (!allowedHeadingLevels.includes(heading.lvl)) {
return;
}
const rawContent = mdToc.titleize(heading.content);
const entry = {
hashLink,
rawContent,
content: md.renderInline(rawContent),
children: [],
};
if (headingLevels.includes(heading.lvl)) {
toc.push(entry);
current = entry;
} else if (current) {
current.children.push(entry);
}
});
return toc;
}
// takes the content of a doc article and returns the content with a table of
// contents inserted
function insertTOC(rawContent) {
if (!rawContent || !tocRegex.test(rawContent)) {
return rawContent;
}
const filterRe = /^`[^`]*`/;
const headers = getTOC(rawContent, 'h3', null);
const tableOfContents = headers
.filter(header => filterRe.test(header.rawContent))
.map(header => ` - [${header.rawContent}](#${header.hashLink})`)
.join('\n');
return rawContent.replace(tocRegex, tableOfContents);
}
module.exports = {
getTOC,
insertTOC,
};

View file

@ -0,0 +1,25 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
// Remove the indentation introduced by JSX
function unindent(code) {
const lines = code.split('\n');
if (lines[0] === '') {
lines.shift();
}
if (lines.length <= 1) {
return code;
}
const indent = lines[0].match(/^\s*/)[0];
for (let i = 0; i < lines.length; ++i) {
lines[i] = lines[i].replace(new RegExp(`^${indent}`), '');
}
return lines.join('\n');
}
module.exports = unindent;

View file

@ -0,0 +1,122 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
const shell = require('shelljs');
const TRUNCATE_MARKER = /<!--\s*truncate\s*-->/;
function blogPostHasTruncateMarker(content) {
return TRUNCATE_MARKER.test(content);
}
function extractBlogPostBeforeTruncate(content) {
return content.split(TRUNCATE_MARKER)[0];
}
function removeExtension(pathStr) {
return pathStr.replace(/\.[^/.]+$/, '');
}
function getPath(pathStr, cleanUrl = false) {
if (!pathStr || !cleanUrl || !pathStr.endsWith('.html')) {
return pathStr;
}
return pathStr.endsWith('/index.html')
? pathStr.replace(/index\.html$/, '')
: removeExtension(pathStr);
}
function idx(target, keyPaths) {
return (
target &&
(Array.isArray(keyPaths)
? keyPaths.reduce((obj, key) => obj && obj[key], target)
: target[keyPaths])
);
}
function getGitLastUpdated(filepath) {
const timestampAndAuthorRegex = /^(\d+), (.+)$/;
function isTimestampAndAuthor(str) {
return timestampAndAuthorRegex.test(str);
}
function getTimestampAndAuthor(str) {
if (!str) {
return null;
}
const temp = str.match(timestampAndAuthorRegex);
return !temp || temp.length < 3
? null
: {timestamp: temp[1], author: temp[2]};
}
// Wrap in try/catch in case the shell commands fail (e.g. project doesn't use Git, etc).
try {
// To differentiate between content change and file renaming / moving, use --summary
// To follow the file history until before it is moved (when we create new version), use
// --follow.
const silentState = shell.config.silent; // Save old silent state.
shell.config.silent = true;
const result = shell
.exec(`git log --follow --summary --format="%ct, %an" ${filepath}`)
.stdout.trim();
shell.config.silent = silentState;
// Format the log results to be
// ['1234567890, Yangshun Tay', 'rename ...', '1234567880,
// 'Joel Marcey', 'move ...', '1234567870', '1234567860']
const records = result
.toString('utf-8')
.replace(/\n\s*\n/g, '\n')
.split('\n')
.filter(String);
const lastContentModifierCommit = records.find((item, index, arr) => {
const currentItemIsTimestampAndAuthor = isTimestampAndAuthor(item);
const isLastTwoItem = index + 2 >= arr.length;
const nextItemIsTimestampAndAuthor = isTimestampAndAuthor(arr[index + 1]);
return (
currentItemIsTimestampAndAuthor &&
(isLastTwoItem || nextItemIsTimestampAndAuthor)
);
});
return lastContentModifierCommit
? getTimestampAndAuthor(lastContentModifierCommit)
: null;
} catch (error) {
console.error(error);
}
return null;
}
function getGitLastUpdatedTime(filepath) {
const commit = getGitLastUpdated(filepath);
if (commit && commit.timestamp) {
const date = new Date(parseInt(commit.timestamp, 10) * 1000);
return date.toLocaleDateString();
}
return null;
}
function getGitLastUpdatedBy(filepath) {
const commit = getGitLastUpdated(filepath);
return commit ? commit.author : null;
}
module.exports = {
blogPostHasTruncateMarker,
extractBlogPostBeforeTruncate,
getGitLastUpdatedTime,
getGitLastUpdatedBy,
getPath,
removeExtension,
idx,
};