WooCommerce Docs: Support Gutenberg block conversion with CommonMark, add some basic unit tests. (#39096)
* Extract docs manifest generation into a CLI tool
This commit is contained in:
parent
dddd0e65ac
commit
e91a72b8a1
|
@ -50,6 +50,10 @@
|
|||
<exclude-pattern>tests/</exclude-pattern>
|
||||
</rule>
|
||||
|
||||
<rule ref="WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents">
|
||||
<exclude-pattern>tests/src</exclude-pattern>
|
||||
</rule>
|
||||
|
||||
<rule ref="WordPress.Files.FileName.InvalidClassFileName">
|
||||
<exclude-pattern>includes/**/abstract-*.php</exclude-pattern>
|
||||
<exclude-pattern>tests/</exclude-pattern>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"phpVersion": "7.4",
|
||||
"plugins": [ "." ],
|
||||
"config": {
|
||||
"WP_DEBUG_LOG": true,
|
||||
"WP_DEBUG_DISPLAY": true
|
||||
},
|
||||
"env": {
|
||||
"development": {},
|
||||
"tests": {
|
||||
"port": 8086,
|
||||
"plugins": [ "." ],
|
||||
"themes": [
|
||||
"https://downloads.wordpress.org/theme/twentynineteen.zip"
|
||||
],
|
||||
"config": {
|
||||
"WP_TESTS_DOMAIN": "localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,9 +12,14 @@ Set up the monorepo as usual, now from this directory run `pnpm build` to build
|
|||
This plugin creates a top level menu called "WooCommerce Docs" that you can navigate to once
|
||||
you've mounted the plugin in your development environment.
|
||||
|
||||
There is a basic script that generates a manifest.json file from a set of example docs. You can run it via:
|
||||
`pnpm generate-manifest`.
|
||||
You can use monorepo utils from the repo root to generate new manifests:
|
||||
|
||||
```
|
||||
pnpm utils md-docs create ./plugins/woocommerce-docs/example-docs woodocs --outputFilePath ./plugins/woocommerce-docs/scripts/manifest.json
|
||||
```
|
||||
|
||||
To load the manifest as a source in the plugin go to the plugin page and add a manifest with url:
|
||||
|
||||
`http://your-local-wp-host/wp-content/plugins/woocommerce-docs/scripts/manifest.json`
|
||||
|
||||
Please note that if you're hosting the file within Docker, that localhost will not work as the host for your file because that's reserved for localhost within the container. You'll need to use the IP address of your machine instead or on Mac OS you can use the Docker DNS name `host.docker.internal`.
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"type": "wordpress-plugin",
|
||||
"require": {
|
||||
"woocommerce/action-scheduler": "^3.6",
|
||||
"erusev/parsedown": "^1.7"
|
||||
"league/commonmark": "^2.4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
@ -12,7 +12,9 @@
|
|||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"woocommerce/woocommerce-sniffs": "^0.1.3"
|
||||
"woocommerce/woocommerce-sniffs": "^0.1.3",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"yoast/phpunit-polyfills": "^2.0"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,3 +7,11 @@ title: What Went Wrong?
|
|||
1. Restart?
|
||||
2. Refresh?
|
||||
3. Profit!
|
||||
|
||||
Try some different things. _Broken?_ Try something else. **Unresponsive?** Try again.
|
||||
|
||||
If you would like to do a search you can go to [A search engine](google.com).
|
||||
|
||||
---
|
||||
|
||||
![An image](https://picsum.photos/200/300 'This is an image.')
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
"build": "wp-scripts build",
|
||||
"start": "wp-scripts start",
|
||||
"postinstall": "composer install",
|
||||
"generate-manifest": "ts-node ./scripts/generate-manifest.ts"
|
||||
"test:env-setup": "wp-env start && wp-env run cli --env-cwd=wp-content/plugins/woocommerce-docs composer install",
|
||||
"test:unit": "pnpm run test:env-setup && wp-env run tests-cli vendor/bin/phpunit --env-cwd=wp-content/plugins/woocommerce-docs"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
@ -27,6 +28,7 @@
|
|||
"@types/react-dom": "^17.0.2",
|
||||
"@woocommerce/dependency-extraction-webpack-plugin": "workspace:*",
|
||||
"@woocommerce/eslint-plugin": "workspace:*",
|
||||
"@wordpress/env": "^8.2.0",
|
||||
"@wordpress/prettier-config": "2.17.0",
|
||||
"@wordpress/scripts": "^26.4.0",
|
||||
"eslint": "^8.32.0",
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
bootstrap="tests/bootstrap.php"
|
||||
backupGlobals="false"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
verbose="true"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd">
|
||||
<testsuites>
|
||||
<testsuite name="WooCommerceDocs Test Suite">
|
||||
<directory suffix=".php">./tests/src</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<coverage includeUncoveredFiles="true">
|
||||
<include>
|
||||
<directory suffix=".php">./src</directory>
|
||||
<file>woocommerce-docs.php</file>
|
||||
</include>
|
||||
</coverage>
|
||||
</phpunit>
|
|
@ -1,115 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import { glob } from 'glob';
|
||||
import crypto from 'crypto';
|
||||
import process from 'process';
|
||||
|
||||
const branch = process.argv[ 2 ] || 'trunk'; // Use 'trunk' as the default branch if no argument is provided
|
||||
|
||||
interface Category {
|
||||
[ key: string ]: unknown;
|
||||
}
|
||||
|
||||
interface Post {
|
||||
[ key: string ]: unknown;
|
||||
}
|
||||
|
||||
function generatePageId( filePath: string, prefix = '' ) {
|
||||
const hash = crypto.createHash( 'sha1' );
|
||||
hash.update( prefix + filePath );
|
||||
return hash.digest( 'hex' );
|
||||
}
|
||||
|
||||
function generateHashOfString( str: string ) {
|
||||
return crypto.createHash( 'sha256' ).update( str ).digest( 'hex' );
|
||||
}
|
||||
|
||||
function generateRawGithubFileUrl(
|
||||
owner: string,
|
||||
repo: string,
|
||||
gitBranch: string,
|
||||
repoPath: string,
|
||||
filePath: string
|
||||
): string {
|
||||
const relativePath = path.relative( repoPath, filePath );
|
||||
const githubUrl = `https://raw.githubusercontent.com/${ owner }/${ repo }/${ gitBranch }/${ relativePath }`;
|
||||
return githubUrl.replace( /\\/g, '/' ); // Ensure we use forward slashes in the URL.
|
||||
}
|
||||
async function processDirectory(
|
||||
directory: string,
|
||||
projectName: string,
|
||||
checkReadme = true
|
||||
): Promise< Category > {
|
||||
let category: Category = {};
|
||||
|
||||
// Process README.md (if exists) for the category definition.
|
||||
const readmePath = path.join( directory, 'README.md' );
|
||||
if ( checkReadme && fs.existsSync( readmePath ) ) {
|
||||
const readmeContent = fs.readFileSync( readmePath, 'utf-8' );
|
||||
const readmeFrontmatter = matter( readmeContent ).data;
|
||||
category = { ...readmeFrontmatter };
|
||||
category.posts = [];
|
||||
}
|
||||
|
||||
const markdownFiles = glob.sync( path.join( directory, '*.md' ) );
|
||||
markdownFiles.forEach( ( filePath ) => {
|
||||
if ( filePath !== readmePath || ! checkReadme ) {
|
||||
// Skip README.md which we have already processed.
|
||||
const fileContent = fs.readFileSync( filePath, 'utf-8' );
|
||||
const fileFrontmatter = matter( fileContent ).data;
|
||||
const post: Post = { ...fileFrontmatter };
|
||||
// @ts-ignore
|
||||
category.posts.push( {
|
||||
...post,
|
||||
url: generateRawGithubFileUrl(
|
||||
'woocommerce',
|
||||
'woocommerce',
|
||||
branch,
|
||||
path.join( __dirname, '../../../' ),
|
||||
filePath
|
||||
),
|
||||
id: generatePageId( filePath, projectName ),
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
// Recursively process subdirectories.
|
||||
category.categories = [];
|
||||
const subdirectories = fs
|
||||
.readdirSync( directory, { withFileTypes: true } )
|
||||
.filter( ( dirent ) => dirent.isDirectory() )
|
||||
.map( ( dirent ) => path.join( directory, dirent.name ) );
|
||||
for ( const subdirectory of subdirectories ) {
|
||||
const subcategory = await processDirectory( subdirectory, projectName );
|
||||
// @ts-ignore
|
||||
category.categories.push( subcategory );
|
||||
}
|
||||
|
||||
return category;
|
||||
}
|
||||
|
||||
async function processRootDirectory( directory: string, projectName: string ) {
|
||||
return processDirectory( directory, projectName, false );
|
||||
}
|
||||
|
||||
// Use the processRootDirectory function.
|
||||
processRootDirectory( path.join( __dirname, '../example-docs' ), 'test-docs' )
|
||||
.then( ( root ) => {
|
||||
const rootHash = generateHashOfString( JSON.stringify( root ) );
|
||||
|
||||
// Add the root hash to the root object.
|
||||
root.hash = rootHash;
|
||||
|
||||
// write it to a file in this directory
|
||||
fs.writeFileSync(
|
||||
path.join( __dirname, 'manifest.json' ),
|
||||
JSON.stringify( root, null, 2 )
|
||||
);
|
||||
} )
|
||||
.catch( ( err ) => {
|
||||
console.error( err );
|
||||
} );
|
|
@ -5,8 +5,8 @@
|
|||
"posts": [
|
||||
{
|
||||
"title": "Local Development",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/add/wc-docs-manifest/plugins/woocommerce-docs/example-docs/get-started/local-development.md",
|
||||
"id": "052b40518676aa0fdf57bc0b71fdca8cd033f151"
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/plugins/woocommerce-docs/example-docs/get-started/local-development.md",
|
||||
"id": "c068ce54044fa44c760a69bd71ef21274f2a5a37"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
|
@ -15,8 +15,8 @@
|
|||
"posts": [
|
||||
{
|
||||
"title": "What Went Wrong?",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/add/wc-docs-manifest/plugins/woocommerce-docs/example-docs/get-started/troubleshooting/what-went-wrong.md",
|
||||
"id": "ce53918354bdeaa9d16db3e0964b56e1699c8220"
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/plugins/woocommerce-docs/example-docs/get-started/troubleshooting/what-went-wrong.md",
|
||||
"id": "1f88c4d039e72c059c928ab475ad1ea0a02c8abb"
|
||||
}
|
||||
],
|
||||
"categories": []
|
||||
|
@ -28,12 +28,12 @@
|
|||
"posts": [
|
||||
{
|
||||
"title": "Unit Testing",
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/add/wc-docs-manifest/plugins/woocommerce-docs/example-docs/testing/unit-tests.md",
|
||||
"id": "c9916c10ae1962ecaa4193ccdadd043241b8ec05"
|
||||
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/plugins/woocommerce-docs/example-docs/testing/unit-tests.md",
|
||||
"id": "120770c899215a889246b47ac883e4dda1f97b8b"
|
||||
}
|
||||
],
|
||||
"categories": []
|
||||
}
|
||||
],
|
||||
"hash": "7c6cdf9772ccf1f9327a35c0ef8e45413085225e20b1df503e2a19a444c106fb"
|
||||
"hash": "180a6ce0bebb8e84072fda451758ef6326490a99e7b3a349b9e45baa8c7c54ac"
|
||||
}
|
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
|
||||
namespace WooCommerceDocs\Blocks;
|
||||
|
||||
use League\CommonMark\Environment\Environment;
|
||||
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
|
||||
use League\CommonMark\MarkdownConverter;
|
||||
|
||||
/**
|
||||
* Class MarkdownParser
|
||||
*/
|
||||
class BlockConverter {
|
||||
|
||||
/** // phpcs:ignore Generic.Commenting.DocComment.MissingShort
|
||||
*
|
||||
* @var MarkdownParser The MarkdownParser instance.
|
||||
*/
|
||||
private $parser;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
$environment = new Environment();
|
||||
$environment->addExtension( new CommonMarkCoreExtension() );
|
||||
$this->parser = new MarkdownConverter( $environment );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert Markdown to Gutenberg blocks.
|
||||
*
|
||||
* @param string $content The Markdown content.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function convert( $content ) {
|
||||
$html = $this->parser->convert( $content )->__toString();
|
||||
return $this->convert_html_to_blocks( $html );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert HTML to blocks.
|
||||
*
|
||||
* @param string $html The HTML content.
|
||||
*/
|
||||
private function convert_html_to_blocks( $html ) {
|
||||
$blocks_html = '';
|
||||
$dom = new \DOMDocument();
|
||||
|
||||
$dom->loadHTML( $html );
|
||||
$xpath = new \DOMXPath( $dom );
|
||||
$nodes = $xpath->query( '//body/*' );
|
||||
|
||||
foreach ( $nodes as $node ) {
|
||||
$blocks_html .= $this->convert_node_to_block( $node );
|
||||
}
|
||||
|
||||
return $blocks_html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a DOM node to a block.
|
||||
*
|
||||
* @param \DOMNode $node The DOM node.
|
||||
*/
|
||||
private function convert_node_to_block( $node ) {
|
||||
$node_name = $node->nodeName; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||
$node_value = $node->nodeValue; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||
$node_content = $this->convert_child_nodes_to_blocks( $node );
|
||||
|
||||
switch ( $node_name ) {
|
||||
case 'p':
|
||||
return $this->create_block( 'paragraph', $node_name, $node_content );
|
||||
case 'h1':
|
||||
return $this->create_block( 'heading', $node_name, $node_content, array( 'level' => 1 ) );
|
||||
case 'h2':
|
||||
return $this->create_block( 'heading', $node_name, $node_content, array( 'level' => 2 ) );
|
||||
case 'h3':
|
||||
return $this->create_block( 'heading', $node_name, $node_content, array( 'level' => 3 ) );
|
||||
case 'h4':
|
||||
return $this->create_block( 'heading', $node_name, $node_content, array( 'level' => 4 ) );
|
||||
case 'h5':
|
||||
return $this->create_block( 'heading', $node_name, $node_content, array( 'level' => 5 ) );
|
||||
case 'h6':
|
||||
return $this->create_block( 'heading', $node_name, $node_content, array( 'level' => 6 ) );
|
||||
case 'ul':
|
||||
return $this->create_block( 'list', $node_name, $node_content, array( 'ordered' => false ) );
|
||||
case 'ol':
|
||||
return $this->create_block( 'list', $node_name, $node_content, array( 'ordered' => true ) );
|
||||
case 'li':
|
||||
return $this->create_block( 'list-item', $node_name, $node_content );
|
||||
case 'hr':
|
||||
return $this->create_block( 'separator', $node_name, null );
|
||||
default:
|
||||
return $this->create_block( 'paragraph', $node_value );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a block.
|
||||
*
|
||||
* @param string $block_name The block name.
|
||||
* @param string $node_name The node name.
|
||||
* @param string $content The content.
|
||||
* @param array $attrs The attributes.
|
||||
*/
|
||||
private function create_block( $block_name, $node_name, $content = null, $attrs = array() ) {
|
||||
$json_attrs = count( $attrs ) > 0 ? ' ' . wp_json_encode( $attrs ) : '';
|
||||
|
||||
$block_html = "<!-- wp:{$block_name}{$json_attrs} -->\n";
|
||||
|
||||
// Special case for hr, at some point we could support other self-closing tags if needed.
|
||||
if ( 'hr' === $node_name ) {
|
||||
$block_html .= "<{$node_name} class=\"wp-block-separator has-alpha-channel-opacity\" />\n";
|
||||
} elseif ( null !== $content ) {
|
||||
$block_html .= "<{$node_name}>{$content}</{$node_name}>\n";
|
||||
}
|
||||
|
||||
$block_html .= "<!-- /wp:{$block_name} -->\n";
|
||||
|
||||
return $block_html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert child nodes to blocks.
|
||||
*
|
||||
* @param \DOMNode $node The DOM node.
|
||||
*/
|
||||
private function convert_child_nodes_to_blocks( $node ) {
|
||||
$content = '';
|
||||
|
||||
foreach ( $node->childNodes as $child_node ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||
$node_type = $child_node->nodeType; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||
$node_name = $child_node->nodeName; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||
|
||||
if ( XML_ELEMENT_NODE === $node_type ) {
|
||||
if ( 'a' === $node_name ) {
|
||||
$href = esc_url( $child_node->getAttribute( 'href' ) );
|
||||
$link_content = $this->convert_child_nodes_to_blocks( $child_node );
|
||||
$content .= "<a href=\"{$href}\">{$link_content}</a>";
|
||||
} elseif ( 'em' === $node_name || 'strong' === $node_name ) {
|
||||
$inline_content = $this->convert_child_nodes_to_blocks( $child_node );
|
||||
$content .= "<{$node_name}>{$inline_content}</{$node_name}>";
|
||||
} elseif ( 'img' === $node_name ) {
|
||||
// Only handle images as inline content for now due to how Markdown is processed by CommonMark.
|
||||
$src = esc_url( $child_node->getAttribute( 'src' ) );
|
||||
$alt = esc_attr( $child_node->getAttribute( 'alt' ) );
|
||||
$content .= "<img src=\"{$src}\" alt=\"{$alt}\" />";
|
||||
} else {
|
||||
$content .= $this->convert_node_to_block( $child_node );
|
||||
}
|
||||
} elseif ( XML_TEXT_NODE === $node_type ) {
|
||||
$content .= $child_node->nodeValue; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
|
||||
}
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace WooCommerceDocs\Manifest;
|
||||
|
||||
use WooCommerceDocs\Blocks\BlockConverter;
|
||||
|
||||
/**
|
||||
* Class ManifestProcessor
|
||||
*
|
||||
|
@ -19,14 +21,14 @@ class ManifestProcessor {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get the parsedown parser
|
||||
* Get the Markdown converter.
|
||||
*/
|
||||
private static function get_parser() {
|
||||
static $parser = null;
|
||||
if ( null === $parser ) {
|
||||
$parser = new \Parsedown();
|
||||
private static function get_converter() {
|
||||
static $converter = null;
|
||||
if ( null === $converter ) {
|
||||
$converter = new BlockConverter();
|
||||
}
|
||||
return $parser;
|
||||
return $converter;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -78,14 +80,14 @@ class ManifestProcessor {
|
|||
$content = preg_replace( '/^---[\s\S]*?---/', '', $content );
|
||||
|
||||
// Parse markdown.
|
||||
$markdown_content = self::get_parser()->text( $content );
|
||||
$blocks = self::get_converter()->convert( $content );
|
||||
|
||||
// If the post doesn't exist, create it.
|
||||
if ( ! $existing_post ) {
|
||||
$post_id = \WooCommerceDocs\Data\DocsStore::insert_docs_post(
|
||||
array(
|
||||
'post_title' => $post['title'],
|
||||
'post_content' => $markdown_content,
|
||||
'post_content' => $blocks,
|
||||
'post_status' => 'publish',
|
||||
),
|
||||
$post['id']
|
||||
|
@ -99,7 +101,7 @@ class ManifestProcessor {
|
|||
array(
|
||||
'ID' => $existing_post->ID,
|
||||
'post_title' => $post['title'],
|
||||
'post_content' => $markdown_content,
|
||||
'post_content' => $blocks,
|
||||
),
|
||||
$post['id']
|
||||
);
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<?php // phpcs:ignore Squiz.Commenting.FileComment.Missing
|
||||
/**
|
||||
* PHPUnit bootstrap file
|
||||
*
|
||||
* @package WooCommerce_Docs
|
||||
*/
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/../vendor/yoast/phpunit-polyfills/phpunitpolyfills-autoload.php';
|
||||
|
||||
$tests_dir = getenv( 'WP_TESTS_DIR' );
|
||||
|
||||
require_once $tests_dir . '/includes/functions.php';
|
||||
require_once $tests_dir . '/includes/bootstrap.php';
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
use WooCommerceDocs\Blocks\BlockConverter;
|
||||
|
||||
/**
|
||||
* Class BlockConverterTest
|
||||
*/
|
||||
class BlockConverterTest extends WP_UnitTestCase {
|
||||
|
||||
/**
|
||||
* Test blocks are converted correctly from sample markdown.
|
||||
*/
|
||||
public function test_blocks_converted() {
|
||||
$block_converter = new BlockConverter();
|
||||
$converted = $block_converter->convert( file_get_contents( __DIR__ . '/fixtures/test.md' ) );
|
||||
$this->assertEquals( $converted, file_get_contents( __DIR__ . '/fixtures/expected.html' ) );
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
<!-- wp:separator -->
|
||||
<hr class="wp-block-separator has-alpha-channel-opacity" />
|
||||
<!-- /wp:separator -->
|
||||
<!-- wp:heading {"level":2} -->
|
||||
<h2>title: Some frontmatter</h2>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:heading {"level":1} -->
|
||||
<h1>Heading 1</h1>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:heading {"level":2} -->
|
||||
<h2>Heading 2</h2>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:heading {"level":3} -->
|
||||
<h3>Heading 3</h3>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:heading {"level":4} -->
|
||||
<h4>Heading 4</h4>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:heading {"level":5} -->
|
||||
<h5>Heading 5</h5>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:heading {"level":6} -->
|
||||
<h6>Heading 6</h6>
|
||||
<!-- /wp:heading -->
|
||||
<!-- wp:list {"ordered":true} -->
|
||||
<ol>
|
||||
<!-- wp:list-item -->
|
||||
<li>Item 1</li>
|
||||
<!-- /wp:list-item -->
|
||||
|
||||
<!-- wp:list-item -->
|
||||
<li>Item 2</li>
|
||||
<!-- /wp:list-item -->
|
||||
|
||||
<!-- wp:list-item -->
|
||||
<li>Item 3</li>
|
||||
<!-- /wp:list-item -->
|
||||
|
||||
</ol>
|
||||
<!-- /wp:list -->
|
||||
<!-- wp:list {"ordered":false} -->
|
||||
<ul>
|
||||
<!-- wp:list-item -->
|
||||
<li>Unordered Item 1</li>
|
||||
<!-- /wp:list-item -->
|
||||
|
||||
<!-- wp:list-item -->
|
||||
<li>Unordered Item 2</li>
|
||||
<!-- /wp:list-item -->
|
||||
|
||||
<!-- wp:list-item -->
|
||||
<li>Unordered Item 3</li>
|
||||
<!-- /wp:list-item -->
|
||||
|
||||
</ul>
|
||||
<!-- /wp:list -->
|
||||
<!-- wp:paragraph -->
|
||||
<p>Try some different things. <em>Italics</em> Try something else. <strong>Bold</strong> Try again.</p>
|
||||
<!-- /wp:paragraph -->
|
||||
<!-- wp:paragraph -->
|
||||
<p>Here is a link: <a href="https://woocommerce.com">Woocommerce.com</a>.</p>
|
||||
<!-- /wp:paragraph -->
|
||||
<!-- wp:separator -->
|
||||
<hr class="wp-block-separator has-alpha-channel-opacity" />
|
||||
<!-- /wp:separator -->
|
||||
<!-- wp:paragraph -->
|
||||
<p><img src="https://picsum.photos/200/300" alt="An image" /></p>
|
||||
<!-- /wp:paragraph -->
|
|
@ -0,0 +1,31 @@
|
|||
---
|
||||
title: Some frontmatter
|
||||
---
|
||||
|
||||
# Heading 1
|
||||
|
||||
## Heading 2
|
||||
|
||||
### Heading 3
|
||||
|
||||
#### Heading 4
|
||||
|
||||
##### Heading 5
|
||||
|
||||
###### Heading 6
|
||||
|
||||
1. Item 1
|
||||
2. Item 2
|
||||
3. Item 3
|
||||
|
||||
- Unordered Item 1
|
||||
- Unordered Item 2
|
||||
- Unordered Item 3
|
||||
|
||||
Try some different things. _Italics_ Try something else. **Bold** Try again.
|
||||
|
||||
Here is a link: [Woocommerce.com](https://woocommerce.com).
|
||||
|
||||
---
|
||||
|
||||
![An image](https://picsum.photos/200/300 'This is an image.')
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
use WooCommerceDocs\Data\ManifestStore;
|
||||
|
||||
/**
|
||||
* Class ManifestStoreTest
|
||||
*/
|
||||
class ManifestStoreTest extends WP_UnitTestCase {
|
||||
|
||||
/**
|
||||
* Test the manifest store stores manifests in a list.
|
||||
*/
|
||||
public function test_adding_a_manifest_stores_it_in_the_list() {
|
||||
ManifestStore::add_manifest( 'https://example.com/manifest.json' );
|
||||
ManifestStore::update_manifest( 'https://example.com/manifest.json', array( 'foo' => 'bar' ) );
|
||||
|
||||
$manifest_list = ManifestStore::get_manifest_list();
|
||||
|
||||
$this->assertEquals( $manifest_list, array( array( 'https://example.com/manifest.json', array( 'foo' => 'bar' ) ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test retrieving a single manifest by url.
|
||||
*/
|
||||
public function test_retrieving_a_single_manifest_by_url() {
|
||||
ManifestStore::add_manifest( 'https://example.com/manifest.json' );
|
||||
ManifestStore::update_manifest( 'https://example.com/manifest.json', array( 'foo' => 'bar' ) );
|
||||
|
||||
$manifest = ManifestStore::get_manifest_by_url( 'https://example.com/manifest.json' );
|
||||
$this->assertEquals( $manifest['foo'], 'bar' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test removing a manifest by url.
|
||||
*/
|
||||
public function test_removing_a_manifest_by_url() {
|
||||
ManifestStore::add_manifest( 'https://example.com/manifest.json' );
|
||||
ManifestStore::remove_manifest( 'https://example.com/manifest.json' );
|
||||
$manifest_list = ManifestStore::get_manifest_list();
|
||||
$this->assertEquals( $manifest_list, array() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test updating an existing manifest.
|
||||
*/
|
||||
public function test_updating_a_manifest() {
|
||||
ManifestStore::add_manifest( 'https://example.com/manifest.json' );
|
||||
ManifestStore::update_manifest( 'https://example.com/manifest.json', array( 'foo' => 'bar' ) );
|
||||
|
||||
$manifest = ManifestStore::get_manifest_by_url( 'https://example.com/manifest.json' );
|
||||
$this->assertEquals( $manifest['foo'], 'bar' );
|
||||
|
||||
ManifestStore::update_manifest( 'https://example.com/manifest.json', array( 'foo' => 'baz' ) );
|
||||
$updated_manifest = ManifestStore::get_manifest_by_url( 'https://example.com/manifest.json' );
|
||||
$this->assertEquals( $updated_manifest['foo'], 'baz' );
|
||||
}
|
||||
}
|
|
@ -3214,6 +3214,9 @@ importers:
|
|||
'@woocommerce/eslint-plugin':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/js/eslint-plugin
|
||||
'@wordpress/env':
|
||||
specifier: ^8.2.0
|
||||
version: 8.2.0
|
||||
'@wordpress/prettier-config':
|
||||
specifier: 2.17.0
|
||||
version: 2.17.0(wp-prettier@2.8.5)
|
||||
|
@ -3468,6 +3471,9 @@ importers:
|
|||
graphql:
|
||||
specifier: ^16.6.0
|
||||
version: 16.6.0
|
||||
gray-matter:
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3
|
||||
octokit:
|
||||
specifier: ^2.0.14
|
||||
version: 2.0.14
|
||||
|
@ -4034,7 +4040,7 @@ packages:
|
|||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
'@babel/core': 7.21.3
|
||||
'@jridgewell/trace-mapping': 0.3.16
|
||||
'@jridgewell/trace-mapping': 0.3.17
|
||||
commander: 4.1.1
|
||||
convert-source-map: 1.8.0
|
||||
fs-readdir-recursive: 1.1.0
|
||||
|
@ -5197,7 +5203,7 @@ packages:
|
|||
dependencies:
|
||||
'@babel/core': 7.12.9
|
||||
'@babel/helper-create-class-features-plugin': 7.19.0(@babel/core@7.12.9)
|
||||
'@babel/helper-plugin-utils': 7.21.5
|
||||
'@babel/helper-plugin-utils': 7.20.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
@ -5209,7 +5215,7 @@ packages:
|
|||
dependencies:
|
||||
'@babel/core': 7.17.8
|
||||
'@babel/helper-create-class-features-plugin': 7.19.0(@babel/core@7.17.8)
|
||||
'@babel/helper-plugin-utils': 7.21.5
|
||||
'@babel/helper-plugin-utils': 7.20.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
@ -6342,7 +6348,7 @@ packages:
|
|||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
'@babel/core': 7.17.8
|
||||
'@babel/helper-plugin-utils': 7.21.5
|
||||
'@babel/helper-plugin-utils': 7.20.2
|
||||
dev: true
|
||||
|
||||
/@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.21.3):
|
||||
|
@ -6351,7 +6357,7 @@ packages:
|
|||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
'@babel/core': 7.21.3
|
||||
'@babel/helper-plugin-utils': 7.21.5
|
||||
'@babel/helper-plugin-utils': 7.20.2
|
||||
|
||||
/@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.12.9):
|
||||
resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==}
|
||||
|
@ -6393,7 +6399,7 @@ packages:
|
|||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
'@babel/core': 7.17.8
|
||||
'@babel/helper-plugin-utils': 7.21.5
|
||||
'@babel/helper-plugin-utils': 7.20.2
|
||||
dev: true
|
||||
|
||||
/@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.21.3):
|
||||
|
@ -6402,7 +6408,7 @@ packages:
|
|||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
'@babel/core': 7.21.3
|
||||
'@babel/helper-plugin-utils': 7.21.5
|
||||
'@babel/helper-plugin-utils': 7.20.2
|
||||
|
||||
/@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.12.9):
|
||||
resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==}
|
||||
|
@ -6897,7 +6903,7 @@ packages:
|
|||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
'@babel/core': 7.17.8
|
||||
'@babel/helper-plugin-utils': 7.21.5
|
||||
'@babel/helper-plugin-utils': 7.20.2
|
||||
dev: true
|
||||
|
||||
/@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.21.3):
|
||||
|
@ -6907,7 +6913,7 @@ packages:
|
|||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
'@babel/core': 7.21.3
|
||||
'@babel/helper-plugin-utils': 7.21.5
|
||||
'@babel/helper-plugin-utils': 7.20.2
|
||||
|
||||
/@babel/plugin-syntax-typescript@7.16.7(@babel/core@7.17.8):
|
||||
resolution: {integrity: sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A==}
|
||||
|
@ -10388,8 +10394,8 @@ packages:
|
|||
'@babel/core': ^7.0.0-0
|
||||
dependencies:
|
||||
'@babel/core': 7.21.3
|
||||
'@babel/helper-plugin-utils': 7.21.5
|
||||
'@babel/helper-validator-option': 7.21.0
|
||||
'@babel/helper-plugin-utils': 7.20.2
|
||||
'@babel/helper-validator-option': 7.18.6
|
||||
'@babel/plugin-transform-flow-strip-types': 7.16.7(@babel/core@7.21.3)
|
||||
dev: true
|
||||
|
||||
|
@ -12240,6 +12246,7 @@ packages:
|
|||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.0
|
||||
'@jridgewell/sourcemap-codec': 1.4.14
|
||||
dev: true
|
||||
|
||||
/@jridgewell/trace-mapping@0.3.17:
|
||||
resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==}
|
||||
|
@ -22182,8 +22189,8 @@ packages:
|
|||
peerDependencies:
|
||||
postcss: ^8.1.0
|
||||
dependencies:
|
||||
browserslist: 4.21.4
|
||||
caniuse-lite: 1.0.30001418
|
||||
browserslist: 4.20.2
|
||||
caniuse-lite: 1.0.30001352
|
||||
fraction.js: 4.2.0
|
||||
normalize-range: 0.1.2
|
||||
picocolors: 1.0.0
|
||||
|
@ -23739,7 +23746,6 @@ packages:
|
|||
escalade: 3.1.1
|
||||
node-releases: 2.0.6
|
||||
picocolors: 1.0.0
|
||||
dev: true
|
||||
|
||||
/browserslist@4.20.4:
|
||||
resolution: {integrity: sha512-ok1d+1WpnU24XYN7oC3QWgTyMhY/avPJ/r9T00xxvUOIparA/gc+UPUMaod3i+G6s+nI2nUb9xZ5k794uIwShw==}
|
||||
|
@ -29694,7 +29700,6 @@ packages:
|
|||
kind-of: 6.0.3
|
||||
section-matter: 1.0.0
|
||||
strip-bom-string: 1.0.0
|
||||
dev: true
|
||||
|
||||
/gridicons@3.4.0(react@17.0.2):
|
||||
resolution: {integrity: sha512-GikyCOcfhwHSN8tfsZvcWwWSaRLebVZCvDzfFg0X50E+dIAnG2phfFUTNa06dXA09kqRYCdnu8sPO8pSYO3UVA==}
|
||||
|
@ -41674,7 +41679,6 @@ packages:
|
|||
dependencies:
|
||||
extend-shallow: 2.0.1
|
||||
kind-of: 6.0.3
|
||||
dev: true
|
||||
|
||||
/seed-random@2.2.0:
|
||||
resolution: {integrity: sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==}
|
||||
|
@ -42649,7 +42653,6 @@ packages:
|
|||
/strip-bom-string@1.0.0:
|
||||
resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
dev: true
|
||||
|
||||
/strip-bom@2.0.0:
|
||||
resolution: {integrity: sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==}
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"figlet": "^1.6.0",
|
||||
"glob": "^10.2.4",
|
||||
"graphql": "^16.6.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"octokit": "^2.0.14",
|
||||
"ora": "^5.4.1",
|
||||
"promptly": "^3.2.0",
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
jest.mock( 'uuid', () => {
|
||||
return {
|
||||
v4: jest.fn( () => 1 ),
|
||||
};
|
||||
} );
|
||||
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
|
|
|
@ -11,6 +11,7 @@ import dotenv from 'dotenv';
|
|||
*/
|
||||
import CodeFreeze from './code-freeze/commands';
|
||||
import Slack from './slack/commands/slack';
|
||||
import Manifest from './md-docs/commands';
|
||||
import Changefile from './changefile';
|
||||
import WorkflowProfiler from './workflow-profiler/commands';
|
||||
import { Logger } from './core/logger';
|
||||
|
@ -32,7 +33,8 @@ const program = new Command()
|
|||
.addCommand( CodeFreeze )
|
||||
.addCommand( Slack )
|
||||
.addCommand( Changefile )
|
||||
.addCommand( WorkflowProfiler );
|
||||
.addCommand( WorkflowProfiler )
|
||||
.addCommand( Manifest );
|
||||
|
||||
program.exitOverride();
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Markdown Docs CLI tool
|
||||
|
||||
This is a CLI tool designed to generate JSON manifests of Markdown files in a directory.
|
||||
|
||||
This manifest is currently designed to be consumed by the [WooCommerce Docs](https://github.com/woocommerce/woocommerce/tree/trunk/plugins/woocommerce-docs) plugin.
|
||||
|
||||
## Usage
|
||||
|
||||
This command is built on postinstall and can be run from monorepo root.
|
||||
|
||||
To create a manifest:
|
||||
|
||||
```
|
||||
pnpm utils md-docs create <path-to-directory> <projectName>
|
||||
```
|
||||
|
||||
### Arguments and options
|
||||
|
||||
To find out more about the arguments and options available, run:
|
||||
|
||||
```
|
||||
pnpm utils md-docs create --help
|
||||
```
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { Command } from '@commander-js/extra-typings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { generateManifestCommand } from './manifest/create';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
|
||||
const program = new Command( 'md-docs' )
|
||||
.description( 'Utilities for generating markdown doc manifests.' )
|
||||
.addCommand( generateManifestCommand, { isDefault: true } );
|
||||
|
||||
export default program;
|
|
@ -0,0 +1,82 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { writeFile } from 'fs';
|
||||
import { Command } from '@commander-js/extra-typings';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { generateManifestFromDirectory } from '../../../lib/generate-manifest';
|
||||
import { Logger } from '../../../../core/logger';
|
||||
|
||||
export const generateManifestCommand = new Command( 'create' )
|
||||
.description(
|
||||
'Create a manifest file representing the contents of a markdown directory.'
|
||||
)
|
||||
.argument(
|
||||
'<directory>',
|
||||
'Path to directory of Markdown files to generate the manifest from.'
|
||||
)
|
||||
.argument(
|
||||
'<projectName>',
|
||||
'Name of the project to generate the manifest for, used to uniquely identify manifest entries.'
|
||||
)
|
||||
.option(
|
||||
'-o --outputFilePath <outputFilePath>',
|
||||
'Full path and filename of where to output the manifest.',
|
||||
`${ process.cwd() }/manifest.json`
|
||||
)
|
||||
.option(
|
||||
'-b --baseUrl <baseUrl>',
|
||||
'Base url to resolve markdown file URLs to in the manifest.',
|
||||
'https://raw.githubusercontent.com/woocommerce/woocommerce/trunk'
|
||||
)
|
||||
.option(
|
||||
'-r --rootDir <rootDir>',
|
||||
'Root directory of the markdown files, used to generate URLs.',
|
||||
process.cwd()
|
||||
)
|
||||
.action( async ( dir, projectName, options ) => {
|
||||
const { outputFilePath, baseUrl, rootDir } = options;
|
||||
|
||||
// determine if the rootDir is absolute or relative
|
||||
const absoluteRootDir = path.isAbsolute( rootDir )
|
||||
? rootDir
|
||||
: path.join( process.cwd(), rootDir );
|
||||
|
||||
const absoluteSubDir = path.isAbsolute( dir )
|
||||
? dir
|
||||
: path.join( process.cwd(), dir );
|
||||
|
||||
const absoluteOutputFilePath = path.isAbsolute( outputFilePath )
|
||||
? outputFilePath
|
||||
: path.join( process.cwd(), outputFilePath );
|
||||
|
||||
Logger.startTask( 'Generating manifest' );
|
||||
|
||||
const manifest = await generateManifestFromDirectory(
|
||||
absoluteRootDir,
|
||||
absoluteSubDir,
|
||||
projectName,
|
||||
baseUrl
|
||||
);
|
||||
|
||||
Logger.endTask();
|
||||
|
||||
Logger.startTask( 'Writing manifest' );
|
||||
|
||||
await writeFile(
|
||||
absoluteOutputFilePath,
|
||||
JSON.stringify( manifest, null, 2 ),
|
||||
( err ) => {
|
||||
if ( err ) {
|
||||
Logger.error( err );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
Logger.endTask();
|
||||
Logger.notice( `Manifest output at ${ outputFilePath }` );
|
||||
} );
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
title: Getting Started with WooCommerce
|
||||
---
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
title: Local Development
|
||||
---
|
||||
|
||||
## Local Development
|
||||
|
||||
1. Install
|
||||
2. Configure
|
||||
3. Profit!
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
title: Troubleshooting Problems
|
||||
---
|
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
title: What Went Wrong?
|
||||
---
|
||||
|
||||
## Try some troubleshooting
|
||||
|
||||
1. Restart?
|
||||
2. Refresh?
|
||||
3. Profit!
|
||||
|
||||
Try some different things. _Broken?_ Try something else. **Unresponsive?** Try again.
|
||||
|
||||
If you would like to do a search you can go to [A search engine](google.com).
|
||||
|
||||
---
|
||||
|
||||
![An image](https://picsum.photos/200/300 'This is an image.')
|
|
@ -0,0 +1,3 @@
|
|||
---
|
||||
title: Testing WooCommerce
|
||||
---
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
title: Unit Testing
|
||||
---
|
||||
|
||||
## Unit Test
|
||||
|
||||
It's simple really, write tests!
|
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { generateFileUrl } from '../generate-manifest';
|
||||
|
||||
describe( 'generateFileUrl', () => {
|
||||
it( 'should generate a file url relative to the root directory provided', () => {
|
||||
const url = generateFileUrl(
|
||||
'https://example.com',
|
||||
path.join( __dirname, 'fixtures/example-docs' ),
|
||||
path.join( __dirname, 'fixtures/example-docs/get-started' ),
|
||||
path.join(
|
||||
__dirname,
|
||||
'fixtures/example-docs/get-started/local-development.md'
|
||||
)
|
||||
);
|
||||
|
||||
expect( url ).toBe(
|
||||
'https://example.com/get-started/local-development.md'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should throw an error if relative paths are passed', () => {
|
||||
expect( () =>
|
||||
generateFileUrl(
|
||||
'https://example.com',
|
||||
'./example-docs',
|
||||
path.join( __dirname, 'fixtures/example-docs/get-started' ),
|
||||
path.join(
|
||||
__dirname,
|
||||
'fixtures/example-docs/get-started/local-development.md'
|
||||
)
|
||||
)
|
||||
).toThrow();
|
||||
|
||||
expect( () =>
|
||||
generateFileUrl(
|
||||
'https://example.com',
|
||||
path.join( __dirname, 'fixtures/example-docs' ),
|
||||
'./get-started',
|
||||
path.join(
|
||||
__dirname,
|
||||
'fixtures/example-docs/get-started/local-development.md'
|
||||
)
|
||||
)
|
||||
).toThrow();
|
||||
|
||||
expect( () =>
|
||||
generateFileUrl(
|
||||
'https://example.com',
|
||||
path.join( __dirname, 'fixtures/example-docs' ),
|
||||
path.join( __dirname, 'fixtures/example-docs/get-started' ),
|
||||
'./local-development.md'
|
||||
)
|
||||
).toThrow();
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { generateManifestFromDirectory } from '../generate-manifest';
|
||||
|
||||
describe( 'generateManifest', () => {
|
||||
const dir = path.join( __dirname, './fixtures/example-docs' );
|
||||
const rootDir = path.join( __dirname, './fixtures' );
|
||||
|
||||
it( 'should generate a manifest with the correct category structure', async () => {
|
||||
// generate the manifest from fixture directory
|
||||
const manifest = await generateManifestFromDirectory(
|
||||
rootDir,
|
||||
dir,
|
||||
'example-docs',
|
||||
'https://example.com'
|
||||
);
|
||||
|
||||
const topLevelCategories = manifest.categories;
|
||||
|
||||
expect( topLevelCategories[ 0 ].title ).toEqual(
|
||||
'Getting Started with WooCommerce'
|
||||
);
|
||||
expect( topLevelCategories[ 1 ].title ).toEqual(
|
||||
'Testing WooCommerce'
|
||||
);
|
||||
|
||||
const subCategories = topLevelCategories[ 0 ].categories;
|
||||
|
||||
expect( subCategories[ 0 ].title ).toEqual(
|
||||
'Troubleshooting Problems'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should create post urls with the correct url', async () => {
|
||||
const manifest = await generateManifestFromDirectory(
|
||||
rootDir,
|
||||
dir,
|
||||
'example-docs',
|
||||
'https://example.com'
|
||||
);
|
||||
|
||||
expect( manifest.categories[ 0 ].posts[ 0 ].url ).toEqual(
|
||||
'https://example.com/example-docs/get-started/local-development.md'
|
||||
);
|
||||
|
||||
expect(
|
||||
manifest.categories[ 0 ].categories[ 0 ].posts[ 0 ].url
|
||||
).toEqual(
|
||||
'https://example.com/example-docs/get-started/troubleshooting/what-went-wrong.md'
|
||||
);
|
||||
} );
|
||||
|
||||
it( 'should create a hash for each manifest', async () => {
|
||||
const manifest = await generateManifestFromDirectory(
|
||||
rootDir,
|
||||
dir,
|
||||
'example-docs',
|
||||
'https://example.com'
|
||||
);
|
||||
|
||||
expect( manifest.hash ).not.toBeUndefined();
|
||||
} );
|
||||
} );
|
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import { glob } from 'glob';
|
||||
import crypto from 'crypto';
|
||||
|
||||
interface Category {
|
||||
[ key: string ]: unknown;
|
||||
posts?: Post[];
|
||||
categories?: Category[];
|
||||
}
|
||||
|
||||
interface Post {
|
||||
[ key: string ]: unknown;
|
||||
}
|
||||
|
||||
function generatePageId( filePath: string, prefix = '' ) {
|
||||
const hash = crypto.createHash( 'sha1' );
|
||||
hash.update( prefix + filePath );
|
||||
return hash.digest( 'hex' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a file url relative to the root directory provided.
|
||||
*
|
||||
* @param baseUrl The base url to use for the file url.
|
||||
* @param rootDirectory The root directory where the file resides.
|
||||
* @param subDirectory The sub-directory where the file resides.
|
||||
* @param absoluteFilePath The absolute path to the file.
|
||||
* @return The file url.
|
||||
*/
|
||||
export const generateFileUrl = (
|
||||
baseUrl: string,
|
||||
rootDirectory: string,
|
||||
subDirectory: string,
|
||||
absoluteFilePath: string
|
||||
) => {
|
||||
// check paths are absolute
|
||||
for ( const filePath of [
|
||||
rootDirectory,
|
||||
subDirectory,
|
||||
absoluteFilePath,
|
||||
] ) {
|
||||
if ( ! path.isAbsolute( filePath ) ) {
|
||||
throw new Error(
|
||||
`File URLs cannot be generated without absolute paths. ${ filePath } is not absolute.`
|
||||
);
|
||||
}
|
||||
}
|
||||
// Generate a path from the subdirectory to the file path.
|
||||
const relativeFilePath = path.resolve( subDirectory, absoluteFilePath );
|
||||
|
||||
// Determine the relative path from the rootDirectory to the filePath.
|
||||
const relativePath = path.relative( rootDirectory, relativeFilePath );
|
||||
|
||||
return `${ baseUrl }/${ relativePath }`;
|
||||
};
|
||||
|
||||
async function processDirectory(
|
||||
rootDirectory: string,
|
||||
subDirectory: string,
|
||||
projectName: string,
|
||||
baseUrl: string,
|
||||
checkReadme = true
|
||||
): Promise< Category > {
|
||||
let category: Category = {};
|
||||
|
||||
// Process README.md (if exists) for the category definition.
|
||||
const readmePath = path.join( subDirectory, 'README.md' );
|
||||
|
||||
if ( checkReadme && fs.existsSync( readmePath ) ) {
|
||||
const readmeContent = fs.readFileSync( readmePath, 'utf-8' );
|
||||
const readmeFrontmatter = matter( readmeContent ).data;
|
||||
category = { ...readmeFrontmatter };
|
||||
category.posts = [];
|
||||
}
|
||||
|
||||
const markdownFiles = glob.sync( path.join( subDirectory, '*.md' ) );
|
||||
|
||||
markdownFiles.forEach( ( filePath ) => {
|
||||
if ( filePath !== readmePath || ! checkReadme ) {
|
||||
// Skip README.md which we have already processed.
|
||||
const fileContent = fs.readFileSync( filePath, 'utf-8' );
|
||||
const fileFrontmatter = matter( fileContent ).data;
|
||||
const post: Post = { ...fileFrontmatter };
|
||||
|
||||
category.posts.push( {
|
||||
...post,
|
||||
url: generateFileUrl(
|
||||
baseUrl,
|
||||
rootDirectory,
|
||||
subDirectory,
|
||||
filePath
|
||||
),
|
||||
id: generatePageId( filePath, projectName ),
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
// Recursively process subdirectories.
|
||||
category.categories = [];
|
||||
const subdirectories = fs
|
||||
.readdirSync( subDirectory, { withFileTypes: true } )
|
||||
.filter( ( dirent ) => dirent.isDirectory() )
|
||||
.map( ( dirent ) => path.join( subDirectory, dirent.name ) );
|
||||
for ( const subdirectory of subdirectories ) {
|
||||
const subcategory = await processDirectory(
|
||||
rootDirectory,
|
||||
subdirectory,
|
||||
projectName,
|
||||
baseUrl
|
||||
);
|
||||
|
||||
category.categories.push( subcategory );
|
||||
}
|
||||
|
||||
return category;
|
||||
}
|
||||
|
||||
export async function generateManifestFromDirectory(
|
||||
rootDirectory: string,
|
||||
subDirectory: string,
|
||||
projectName: string,
|
||||
baseUrl: string
|
||||
) {
|
||||
const manifest = await processDirectory(
|
||||
rootDirectory,
|
||||
subDirectory,
|
||||
projectName,
|
||||
baseUrl,
|
||||
false
|
||||
);
|
||||
|
||||
// Generate hash of the manifest contents.
|
||||
const hash = crypto
|
||||
.createHash( 'sha256' )
|
||||
.update( JSON.stringify( manifest ) )
|
||||
.digest( 'hex' );
|
||||
|
||||
return { ...manifest, hash };
|
||||
}
|
Loading…
Reference in New Issue