WooCommerce Docs: Improve Manifest Structure, Extend Frontmatter Support (#39214)

This commit is contained in:
Sam Seay 2023-07-17 16:46:23 +08:00 committed by GitHub
parent 66cee083d4
commit 69e9acaba9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 548 additions and 133 deletions

View File

@ -14,7 +14,8 @@
"require-dev": { "require-dev": {
"woocommerce/woocommerce-sniffs": "^0.1.3", "woocommerce/woocommerce-sniffs": "^0.1.3",
"phpunit/phpunit": "^9.6", "phpunit/phpunit": "^9.6",
"yoast/phpunit-polyfills": "^2.0" "yoast/phpunit-polyfills": "^2.0",
"php-stubs/wordpress-tests-stubs": "^6.2"
}, },
"config": { "config": {
"allow-plugins": { "allow-plugins": {

View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "c3e8ed8878c0f36e0b6be7c78d35663f", "content-hash": "061f99e607a2e8a793f0231a294cc761",
"packages": [ "packages": [
{ {
"name": "dflydev/dot-access-data", "name": "dflydev/dot-access-data",
@ -1030,6 +1030,46 @@
}, },
"time": "2022-02-21T01:04:05+00:00" "time": "2022-02-21T01:04:05+00:00"
}, },
{
"name": "php-stubs/wordpress-tests-stubs",
"version": "v6.2.0",
"source": {
"type": "git",
"url": "https://github.com/php-stubs/wordpress-tests-stubs.git",
"reference": "175f395c814d9f52ebd2c1c64069a3b01ef764e8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-stubs/wordpress-tests-stubs/zipball/175f395c814d9f52ebd2c1c64069a3b01ef764e8",
"reference": "175f395c814d9f52ebd2c1c64069a3b01ef764e8",
"shasum": ""
},
"require-dev": {
"php": "~7.3 || ~8.0",
"php-stubs/generator": "^0.8.0"
},
"suggest": {
"symfony/polyfill-php73": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions",
"szepeviktor/phpstan-wordpress": "WordPress extensions for PHPStan"
},
"type": "library",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "WordPress Tests function and class declaration stubs for static analysis.",
"homepage": "https://github.com/php-stubs/wordpress-tests-stubs",
"keywords": [
"PHPStan",
"static analysis",
"wordpress"
],
"support": {
"issues": "https://github.com/php-stubs/wordpress-tests-stubs/issues",
"source": "https://github.com/php-stubs/wordpress-tests-stubs/tree/v6.2.0"
},
"time": "2023-05-15T07:50:52+00:00"
},
{ {
"name": "phpcompatibility/php-compatibility", "name": "phpcompatibility/php-compatibility",
"version": "9.3.5", "version": "9.3.5",

View File

@ -1,3 +1,3 @@
--- ---
title: Getting Started with WooCommerce category_title: Getting Started with WooCommerce
--- ---

View File

@ -0,0 +1,9 @@
---
post_title: Install the Plugin
---
## Install the plugin
1. Download the plugin from the [GitHub repository](https://example.com).
2. Upload the plugin to your WordPress site.
3. Activate the plugin.

View File

@ -1,5 +1,5 @@
--- ---
title: Local Development post_title: Local Development
--- ---
## Local Development ## Local Development

View File

@ -1,3 +1,3 @@
--- ---
title: Troubleshooting Problems category_title: Troubleshooting Problems
--- ---

View File

@ -1,5 +1,5 @@
--- ---
title: What Went Wrong? post_title: What Went Wrong?
--- ---
## Try some troubleshooting ## Try some troubleshooting

View File

@ -1,3 +1,3 @@
--- ---
title: Testing WooCommerce category_title: Testing WooCommerce
--- ---

View File

@ -1,5 +1,5 @@
--- ---
title: Unit Testing post_title: Unit Testing
--- ---
## Unit Test ## Unit Test

View File

@ -1,39 +1,53 @@
{ {
"categories": [ "categories": [
{ {
"title": "Getting Started with WooCommerce", "category_title": "Getting Started with WooCommerce",
"posts": [ "posts": [
{ {
"title": "Local Development", "post_title": "Local Development",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/plugins/woocommerce-docs/example-docs/get-started/local-development.md", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/plugins/woocommerce-docs/example-docs/get-started/local-development.md",
"id": "c068ce54044fa44c760a69bd71ef21274f2a5a37" "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/plugins/woocommerce-docs/example-docs/get-started/local-development.md",
} "id": "c068ce54044fa44c760a69bd71ef21274f2a5a37"
], }
"categories": [ ],
{ "categories": [
"title": "Troubleshooting Problems", {
"posts": [ "category_title": "Installation",
{ "posts": [
"title": "What Went Wrong?", {
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/plugins/woocommerce-docs/example-docs/get-started/troubleshooting/what-went-wrong.md", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/plugins/woocommerce-docs/example-docs/get-started/installation/install-plugin.md",
"id": "1f88c4d039e72c059c928ab475ad1ea0a02c8abb" "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/plugins/woocommerce-docs/example-docs/get-started/installation/install-plugin.md",
} "id": "fb59bd01dda7b090e5b0a557948e155a6b679d6a"
], }
"categories": [] ],
} "categories": []
] },
}, {
{ "category_title": "Troubleshooting Problems",
"title": "Testing WooCommerce", "posts": [
"posts": [ {
{ "post_title": "What Went Wrong?",
"title": "Unit Testing", "edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/plugins/woocommerce-docs/example-docs/get-started/troubleshooting/what-went-wrong.md",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/plugins/woocommerce-docs/example-docs/testing/unit-tests.md", "url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/plugins/woocommerce-docs/example-docs/get-started/troubleshooting/what-went-wrong.md",
"id": "120770c899215a889246b47ac883e4dda1f97b8b" "id": "1f88c4d039e72c059c928ab475ad1ea0a02c8abb"
} }
], ],
"categories": [] "categories": []
} }
], ]
"hash": "180a6ce0bebb8e84072fda451758ef6326490a99e7b3a349b9e45baa8c7c54ac" },
{
"category_title": "Testing WooCommerce",
"posts": [
{
"post_title": "Unit Testing",
"edit_url": "https://github.com/woocommerce/woocommerce/edit/trunk/plugins/woocommerce-docs/example-docs/testing/unit-tests.md",
"url": "https://raw.githubusercontent.com/woocommerce/woocommerce/trunk/plugins/woocommerce-docs/example-docs/testing/unit-tests.md",
"id": "120770c899215a889246b47ac883e4dda1f97b8b"
}
],
"categories": []
}
],
"hash": "db0047299d7dd99bd9a657d59be9e6e4f113710a253c27417427965f7a5ebc8c"
} }

View File

@ -31,6 +31,39 @@ class ManifestProcessor {
return $converter; return $converter;
} }
/**
* Generate post args
*
* @param mixed $post The post to generate args for.
* @param mixed $post_content The post content.
* @return array
*/
private static function generate_post_args( $post, $post_content ) {
$possible_attributes = array(
'post_title',
'post_author',
'post_date',
'comment_status',
'post_status',
);
$args = array();
foreach ( $possible_attributes as $attribute ) {
if ( isset( $post[ $attribute ] ) ) {
$args[ $attribute ] = $post[ $attribute ];
}
}
$args['post_content'] = $post_content;
if ( ! isset( $args['post_status'] ) ) {
$args['post_status'] = 'publish';
}
return $args;
}
/** /**
* Process categories * Process categories
* *
@ -40,25 +73,27 @@ class ManifestProcessor {
*/ */
private static function process_categories( $categories, $logger_action_id, $parent_id = 0 ) { private static function process_categories( $categories, $logger_action_id, $parent_id = 0 ) {
foreach ( $categories as $category ) { foreach ( $categories as $category ) {
$term = term_exists( $category['title'], 'category' ); $term = term_exists( $category['category_title'], 'category' );
$category_args = array( 'parent' => $parent_id );
if ( isset( $category['category_slug'] ) ) {
$category_args['slug'] = $category['category_slug'];
}
// If the category doesn't exist, create it. // If the category doesn't exist, create it.
if ( 0 === $term || null === $term ) { if ( 0 === $term || null === $term ) {
$term = wp_insert_term( $term = wp_insert_term(
$category['title'], $category['category_title'],
'category', 'category',
array( $category_args
'parent' => $parent_id,
)
); );
} else { } else {
// If the category exists, update it. // If the category exists, update it.
$term = wp_update_term( $term = wp_update_term(
$term['term_id'], $term['term_id'],
'category', 'category',
array( $category_args
'parent' => $parent_id,
)
); );
} }
@ -85,31 +120,27 @@ class ManifestProcessor {
// If the post doesn't exist, create it. // If the post doesn't exist, create it.
if ( ! $existing_post ) { if ( ! $existing_post ) {
$post_id = \WooCommerceDocs\Data\DocsStore::insert_docs_post( $post_id = \WooCommerceDocs\Data\DocsStore::insert_docs_post(
array( self::generate_post_args( $post, $blocks ),
'post_title' => $post['title'],
'post_content' => $blocks,
'post_status' => 'publish',
),
$post['id'] $post['id']
); );
\ActionScheduler_Logger::instance()->log( $logger_action_id, 'Created post with id: ' . $post_id ); \ActionScheduler_Logger::instance()->log( $logger_action_id, 'Created post with id: ' . $post_id );
} else { } else {
$post_update = self::generate_post_args( $post, $blocks );
$post_update = array_merge( $post_update, array( 'ID' => $existing_post->ID ) );
// if the post exists, update it . // if the post exists, update it .
$post_id = \WoocommerceDocs\Data\DocsStore::update_docs_post( $post_id = \WoocommerceDocs\Data\DocsStore::update_docs_post(
array( $post_update,
'ID' => $existing_post->ID,
'post_title' => $post['title'],
'post_content' => $blocks,
),
$post['id'] $post['id']
); );
\ActionScheduler_Logger::instance()->log( $logger_action_id, 'Updated post with id: ' . $post_id ); \ActionScheduler_Logger::instance()->log( $logger_action_id, 'Updated post with id: ' . $post_id );
} }
wp_set_post_categories( $post_id, array( $term['term_id'] ), $parent_id ); wp_set_post_categories( $post_id, array( $term['term_id'] ) );
} }
// Process any sub-categories. // Process any sub-categories.

View File

@ -11,3 +11,6 @@ $tests_dir = getenv( 'WP_TESTS_DIR' );
require_once $tests_dir . '/includes/functions.php'; require_once $tests_dir . '/includes/functions.php';
require_once $tests_dir . '/includes/bootstrap.php'; require_once $tests_dir . '/includes/bootstrap.php';
// Require action-scheduler manually.
require_once __DIR__ . '/../vendor/woocommerce/action-scheduler/action-scheduler.php';

View File

@ -0,0 +1,79 @@
<?php
namespace WooCommerceDocs\Tests\Manifest;
use WooCommerceDocs\Manifest\ManifestProcessor;
use WP_UnitTestCase;
use WooCommerceDocs\Data\DocsStore;
/**
* Class ManifestProcessorTest
*
* @package WooCommerceDocs\Tests\Manifest
*/
class ManifestProcessorTest extends WP_UnitTestCase {
/**
* Test processing a manifest into WordPress posts and categories.
*/
public function test_process_manifest() {
$manifest = json_decode( file_get_contents( __DIR__ . '/fixtures/manifest.json' ), true );
$md_file = file_get_contents( __DIR__ . '/fixtures/test.md' );
// Mock the wp_remote_get function with a filter.
add_filter(
'pre_http_request',
function ( $preempt, $args, $url ) use ( $md_file ) {
return array(
'response' => array(
'code' => 200,
'message' => 'OK',
),
'body' => $md_file,
);
},
10,
3
);
ManifestProcessor::process_manifest( $manifest, 123 );
$get_started_category = get_term_by( 'name', 'Getting Started with WooCommerce', 'category' );
$this->assertNotFalse( $get_started_category );
$install_category = get_term_by( 'name', 'Installation', 'category' );
$this->assertNotFalse( $install_category );
// Check parent is correct.
$this->assertEquals( $get_started_category->term_id, $install_category->parent );
$troubleshoot_category = get_term_by( 'name', 'Troubleshooting Problems', 'category' );
$this->assertNotFalse( $troubleshoot_category );
// Check parent is correct.
$this->assertEquals( $get_started_category->term_id, $troubleshoot_category->parent );
$testing_category = get_term_by( 'name', 'Testing WooCommerce', 'category' );
$this->assertNotFalse( $testing_category );
// check posts exist using the DocStore.
$posts = DocsStore::get_posts();
$this->assertEquals( 4, count( $posts ) );
$install_plugin_post = $posts[0];
$local_dev_post = $posts[1];
$unit_testing_post = $posts[2];
$what_went_wrong_post = $posts[3];
// check doc titles (note that they are returned in alpha order).
$this->assertEquals( 'Install the Plugin', $install_plugin_post->post_title );
$this->assertEquals( 'Local Development', $local_dev_post->post_title );
$this->assertEquals( 'Unit Testing', $unit_testing_post->post_title );
$this->assertEquals( 'What Went Wrong?', $what_went_wrong_post->post_title );
// Assert that post was assigned to categories.
$this->assertTrue( has_category( $install_category->term_id, $install_plugin_post->ID ) );
$this->assertTrue( has_category( $troubleshoot_category->term_id, $what_went_wrong_post->ID ) );
$this->assertTrue( has_category( $testing_category->term_id, $unit_testing_post->ID ) );
$this->assertTrue( has_category( $troubleshoot_category->term_id, $what_went_wrong_post->ID ) );
}
}

View File

@ -0,0 +1,54 @@
{
"categories": [
{
"category_title": "Getting Started with WooCommerce",
"posts": [
{
"post_title": "Local Development",
"edit_url": "https://example.com/edit/local-development.md",
"url": "https://example.com/local-development.md",
"id": "c068ce54044fa44c760a69bd71ef21274f2a5a37"
}
],
"categories": [
{
"category_title": "Installation",
"posts": [
{
"post_title": "Install the Plugin",
"edit_url": "https://example.com/edit/install-plugin.md",
"url": "https://example.com/install-plugin.md",
"id": "fb59bd01dda7b090e5b0a557948e155a6b679d6a"
}
],
"categories": []
},
{
"category_title": "Troubleshooting Problems",
"posts": [
{
"post_title": "What Went Wrong?",
"edit_url": "https://example.com/edit/what-went-wrong.md",
"url": "https://example.com/what-went-wrong.md",
"id": "1f88c4d039e72c059c928ab475ad1ea0a02c8abb"
}
],
"categories": []
}
]
},
{
"category_title": "Testing WooCommerce",
"posts": [
{
"post_title": "Unit Testing",
"edit_url": "https://example.com/edit/unit-tests.md",
"url": "https://example.com/unit-tests.md",
"id": "120770c899215a889246b47ac883e4dda1f97b8b"
}
],
"categories": []
}
],
"hash": "db0047299d7dd99bd9a657d59be9e6e4f113710a253c27417427965f7a5ebc8c"
}

View File

@ -0,0 +1,7 @@
---
post_title: Testing Post
---
### Testing Post
This is a test post.

View File

@ -3474,6 +3474,9 @@ importers:
gray-matter: gray-matter:
specifier: ^4.0.3 specifier: ^4.0.3
version: 4.0.3 version: 4.0.3
js-yaml:
specifier: ^4.1.0
version: 4.1.0
octokit: octokit:
specifier: ^2.0.14 specifier: ^2.0.14
version: 2.0.14 version: 2.0.14
@ -9184,8 +9187,8 @@ packages:
dependencies: dependencies:
'@babel/core': 7.21.3 '@babel/core': 7.21.3
'@babel/helper-module-imports': 7.16.0 '@babel/helper-module-imports': 7.16.0
'@babel/helper-plugin-utils': 7.14.5 '@babel/helper-plugin-utils': 7.21.5
babel-plugin-polyfill-corejs2: 0.3.0(@babel/core@7.21.3) babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.21.3)
babel-plugin-polyfill-corejs3: 0.4.0(@babel/core@7.21.3) babel-plugin-polyfill-corejs3: 0.4.0(@babel/core@7.21.3)
babel-plugin-polyfill-regenerator: 0.3.0(@babel/core@7.21.3) babel-plugin-polyfill-regenerator: 0.3.0(@babel/core@7.21.3)
semver: 6.3.0 semver: 6.3.0
@ -18453,7 +18456,7 @@ packages:
'@wordpress/style-engine': 0.15.0 '@wordpress/style-engine': 0.15.0
'@wordpress/token-list': 2.19.0 '@wordpress/token-list': 2.19.0
'@wordpress/url': 3.29.0 '@wordpress/url': 3.29.0
'@wordpress/warning': 2.34.0 '@wordpress/warning': 2.19.0
'@wordpress/wordcount': 3.19.0 '@wordpress/wordcount': 3.19.0
change-case: 4.1.2 change-case: 4.1.2
classnames: 2.3.1 classnames: 2.3.1
@ -19813,7 +19816,7 @@ packages:
cosmiconfig: 7.0.1 cosmiconfig: 7.0.1
eslint: 8.32.0 eslint: 8.32.0
eslint-config-prettier: 8.5.0(eslint@8.32.0) eslint-config-prettier: 8.5.0(eslint@8.32.0)
eslint-plugin-import: 2.25.4(@typescript-eslint/parser@5.54.0)(eslint-import-resolver-typescript@2.5.0)(eslint-import-resolver-webpack@0.13.2)(eslint@8.32.0) eslint-plugin-import: 2.25.4(@typescript-eslint/parser@5.54.0)(eslint@8.32.0)
eslint-plugin-jest: 27.2.1(@typescript-eslint/eslint-plugin@5.54.0)(eslint@8.32.0)(jest@27.5.1)(typescript@4.9.5) eslint-plugin-jest: 27.2.1(@typescript-eslint/eslint-plugin@5.54.0)(eslint@8.32.0)(jest@27.5.1)(typescript@4.9.5)
eslint-plugin-jsdoc: 39.9.1(eslint@8.32.0) eslint-plugin-jsdoc: 39.9.1(eslint@8.32.0)
eslint-plugin-jsx-a11y: 6.5.1(eslint@8.32.0) eslint-plugin-jsx-a11y: 6.5.1(eslint@8.32.0)
@ -19854,7 +19857,7 @@ packages:
cosmiconfig: 7.0.1 cosmiconfig: 7.0.1
eslint: 8.32.0 eslint: 8.32.0
eslint-config-prettier: 8.5.0(eslint@8.32.0) eslint-config-prettier: 8.5.0(eslint@8.32.0)
eslint-plugin-import: 2.25.4(@typescript-eslint/parser@5.54.0)(eslint-import-resolver-typescript@2.5.0)(eslint-import-resolver-webpack@0.13.2)(eslint@8.32.0) eslint-plugin-import: 2.25.4(@typescript-eslint/parser@5.54.0)(eslint@8.32.0)
eslint-plugin-jest: 27.2.1(@typescript-eslint/eslint-plugin@5.54.0)(eslint@8.32.0)(jest@27.5.1)(typescript@4.9.5) eslint-plugin-jest: 27.2.1(@typescript-eslint/eslint-plugin@5.54.0)(eslint@8.32.0)(jest@27.5.1)(typescript@4.9.5)
eslint-plugin-jsdoc: 39.9.1(eslint@8.32.0) eslint-plugin-jsdoc: 39.9.1(eslint@8.32.0)
eslint-plugin-jsx-a11y: 6.5.1(eslint@8.32.0) eslint-plugin-jsx-a11y: 6.5.1(eslint@8.32.0)
@ -19895,7 +19898,7 @@ packages:
cosmiconfig: 7.0.1 cosmiconfig: 7.0.1
eslint: 8.32.0 eslint: 8.32.0
eslint-config-prettier: 8.5.0(eslint@8.32.0) eslint-config-prettier: 8.5.0(eslint@8.32.0)
eslint-plugin-import: 2.25.4(@typescript-eslint/parser@5.54.0)(eslint-import-resolver-typescript@2.5.0)(eslint-import-resolver-webpack@0.13.2)(eslint@8.32.0) eslint-plugin-import: 2.25.4(@typescript-eslint/parser@5.54.0)(eslint@8.32.0)
eslint-plugin-jest: 27.2.1(@typescript-eslint/eslint-plugin@5.54.0)(eslint@8.32.0)(jest@29.5.0)(typescript@4.9.5) eslint-plugin-jest: 27.2.1(@typescript-eslint/eslint-plugin@5.54.0)(eslint@8.32.0)(jest@29.5.0)(typescript@4.9.5)
eslint-plugin-jsdoc: 39.9.1(eslint@8.32.0) eslint-plugin-jsdoc: 39.9.1(eslint@8.32.0)
eslint-plugin-jsx-a11y: 6.5.1(eslint@8.32.0) eslint-plugin-jsx-a11y: 6.5.1(eslint@8.32.0)
@ -27112,7 +27115,7 @@ packages:
has: 1.0.3 has: 1.0.3
is-core-module: 2.8.0 is-core-module: 2.8.0
is-glob: 4.0.3 is-glob: 4.0.3
minimatch: 3.0.4 minimatch: 3.1.2
object.values: 1.1.5 object.values: 1.1.5
resolve: 1.20.0 resolve: 1.20.0
tsconfig-paths: 3.14.0 tsconfig-paths: 3.14.0
@ -27152,6 +27155,36 @@ packages:
- eslint-import-resolver-webpack - eslint-import-resolver-webpack
- supports-color - supports-color
/eslint-plugin-import@2.25.4(@typescript-eslint/parser@5.54.0)(eslint@8.32.0):
resolution: {integrity: sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==}
engines: {node: '>=4'}
peerDependencies:
'@typescript-eslint/parser': '*'
eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8
peerDependenciesMeta:
'@typescript-eslint/parser':
optional: true
dependencies:
'@typescript-eslint/parser': 5.54.0(eslint@8.32.0)(typescript@4.9.5)
array-includes: 3.1.4
array.prototype.flat: 1.2.5
debug: 2.6.9(supports-color@6.1.0)
doctrine: 2.1.0
eslint: 8.32.0
eslint-import-resolver-node: 0.3.6
eslint-module-utils: 2.7.3(@typescript-eslint/parser@5.54.0)(eslint-import-resolver-node@0.3.6)(eslint-import-resolver-typescript@2.5.0)(eslint-import-resolver-webpack@0.13.2)
has: 1.0.3
is-core-module: 2.8.0
is-glob: 4.0.3
minimatch: 3.1.2
object.values: 1.1.5
resolve: 1.20.0
tsconfig-paths: 3.14.0
transitivePeerDependencies:
- eslint-import-resolver-typescript
- eslint-import-resolver-webpack
- supports-color
/eslint-plugin-jest@23.20.0(eslint@7.32.0)(typescript@4.9.5): /eslint-plugin-jest@23.20.0(eslint@7.32.0)(typescript@4.9.5):
resolution: {integrity: sha512-+6BGQt85OREevBDWCvhqj1yYA4+BFK4XnRZSGJionuEYmcglMZYLNNBBemwzbqUAckURaHdJSBcjHPyrtypZOw==} resolution: {integrity: sha512-+6BGQt85OREevBDWCvhqj1yYA4+BFK4XnRZSGJionuEYmcglMZYLNNBBemwzbqUAckURaHdJSBcjHPyrtypZOw==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -34072,7 +34105,7 @@ packages:
esprima: 4.0.1 esprima: 4.0.1
/js-yaml@3.5.3: /js-yaml@3.5.3:
resolution: {integrity: sha1-6e5ggrBld3DkNG368qWMWZIlH3Y=} resolution: {integrity: sha512-rkxjJUwevxyYOdr45k3IOyZjyhRbEMqUvcMyXXeBXTf8kdFaFKUIvMkdHgIcFIjNIoctM6l/emA7OjXYVabYSw==}
hasBin: true hasBin: true
dependencies: dependencies:
argparse: 1.0.10 argparse: 1.0.10

View File

@ -23,6 +23,7 @@
"glob": "^10.2.4", "glob": "^10.2.4",
"graphql": "^16.6.0", "graphql": "^16.6.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"js-yaml": "^4.1.0",
"octokit": "^2.0.14", "octokit": "^2.0.14",
"ora": "^5.4.1", "ora": "^5.4.1",
"promptly": "^3.2.0", "promptly": "^3.2.0",

View File

@ -38,8 +38,13 @@ export const generateManifestCommand = new Command( 'create' )
'Root directory of the markdown files, used to generate URLs.', 'Root directory of the markdown files, used to generate URLs.',
process.cwd() process.cwd()
) )
.option(
'-be --baseEditUrl <baseEditUrl>',
'Base url to provide edit links to. This option will be ignored if your baseUrl is not a GitHub URL.',
'https://github.com/woocommerce/woocommerce/edit/trunk'
)
.action( async ( dir, projectName, options ) => { .action( async ( dir, projectName, options ) => {
const { outputFilePath, baseUrl, rootDir } = options; const { outputFilePath, baseUrl, rootDir, baseEditUrl } = options;
// determine if the rootDir is absolute or relative // determine if the rootDir is absolute or relative
const absoluteRootDir = path.isAbsolute( rootDir ) const absoluteRootDir = path.isAbsolute( rootDir )
@ -60,7 +65,8 @@ export const generateManifestCommand = new Command( 'create' )
absoluteRootDir, absoluteRootDir,
absoluteSubDir, absoluteSubDir,
projectName, projectName,
baseUrl baseUrl,
baseEditUrl
); );
Logger.endTask(); Logger.endTask();

View File

@ -1,3 +1,4 @@
--- ---
title: Getting Started with WooCommerce category_title: Getting Started with WooCommerce
category_slug: get-started
--- ---

View File

@ -0,0 +1,5 @@
### Install the plugin
1. Download the plugin from the [GitHub repository](https://example.com).
2. Upload the plugin to your WordPress site.
3. Activate the plugin.

View File

@ -1,5 +1,5 @@
--- ---
title: Local Development post_title: Local Development
--- ---
## Local Development ## Local Development

View File

@ -1,3 +1,3 @@
--- ---
title: Troubleshooting Problems category_title: Troubleshooting Problems
--- ---

View File

@ -1,5 +1,5 @@
--- ---
title: What Went Wrong? post_title: What Went Wrong?
--- ---
## Try some troubleshooting ## Try some troubleshooting

View File

@ -1,3 +1,3 @@
--- ---
title: Testing WooCommerce category_title: Testing WooCommerce
--- ---

View File

@ -1,5 +1,5 @@
--- ---
title: Unit Testing category_title: Unit Testing
--- ---
## Unit Test ## Unit Test

View File

@ -6,7 +6,7 @@ import path from 'path';
/** /**
* Internal dependencies * Internal dependencies
*/ */
import { generateFileUrl } from '../generate-manifest'; import { generateFileUrl } from '../generate-urls';
describe( 'generateFileUrl', () => { describe( 'generateFileUrl', () => {
it( 'should generate a file url relative to the root directory provided', () => { it( 'should generate a file url relative to the root directory provided', () => {

View File

@ -0,0 +1,31 @@
/**
* Internal dependencies
*/
import { generatePostFrontMatter } from '../generate-frontmatter';
describe( 'generateFrontmatter', () => {
it( 'should not allow disallowed attributes', () => {
const frontMatter = generatePostFrontMatter( `---
title: Hello World
description: This is a description
post_content: This is some content
post_title: This is a title
---
` );
expect( frontMatter ).toEqual( {
post_title: 'This is a title',
} );
} );
it( 'should not do additional date parsing', () => {
const frontMatter = generatePostFrontMatter( `---
post_date: 2023-07-12 15:30:00
---
` );
expect( frontMatter ).toEqual( {
post_date: '2023-07-12 15:30:00',
} );
} );
} );

View File

@ -18,31 +18,47 @@ describe( 'generateManifest', () => {
rootDir, rootDir,
dir, dir,
'example-docs', 'example-docs',
'https://example.com' 'https://example.com',
'https://example.com/edit'
); );
const topLevelCategories = manifest.categories; const topLevelCategories = manifest.categories;
expect( topLevelCategories[ 0 ].title ).toEqual( expect( topLevelCategories[ 0 ].category_title ).toEqual(
'Getting Started with WooCommerce' 'Getting Started with WooCommerce'
); );
expect( topLevelCategories[ 1 ].title ).toEqual( expect( topLevelCategories[ 1 ].category_title ).toEqual(
'Testing WooCommerce' 'Testing WooCommerce'
); );
const subCategories = topLevelCategories[ 0 ].categories; const subCategories = topLevelCategories[ 0 ].categories;
expect( subCategories[ 0 ].title ).toEqual( expect( subCategories[ 1 ].category_title ).toEqual(
'Troubleshooting Problems' 'Troubleshooting Problems'
); );
} ); } );
it( 'should create categories with titles where there is no index README', async () => {
const manifest = await generateManifestFromDirectory(
rootDir,
dir,
'example-docs',
'https://example.com',
'https://example.com/edit'
);
expect(
manifest.categories[ 0 ].categories[ 0 ].category_title
).toEqual( 'Installation' );
} );
it( 'should create post urls with the correct url', async () => { it( 'should create post urls with the correct url', async () => {
const manifest = await generateManifestFromDirectory( const manifest = await generateManifestFromDirectory(
rootDir, rootDir,
dir, dir,
'example-docs', 'example-docs',
'https://example.com' 'https://example.com',
'https://example.com/edit'
); );
expect( manifest.categories[ 0 ].posts[ 0 ].url ).toEqual( expect( manifest.categories[ 0 ].posts[ 0 ].url ).toEqual(
@ -52,7 +68,7 @@ describe( 'generateManifest', () => {
expect( expect(
manifest.categories[ 0 ].categories[ 0 ].posts[ 0 ].url manifest.categories[ 0 ].categories[ 0 ].posts[ 0 ].url
).toEqual( ).toEqual(
'https://example.com/example-docs/get-started/troubleshooting/what-went-wrong.md' 'https://example.com/example-docs/get-started/installation/install-plugin.md'
); );
} ); } );
@ -61,9 +77,24 @@ describe( 'generateManifest', () => {
rootDir, rootDir,
dir, dir,
'example-docs', 'example-docs',
'https://example.com' 'https://example.com',
'https://example.com/edit'
); );
expect( manifest.hash ).not.toBeUndefined(); expect( manifest.hash ).not.toBeUndefined();
} ); } );
it( 'should generate edit_url when github is in the base url', async () => {
const manifest = await generateManifestFromDirectory(
rootDir,
dir,
'example-docs',
'https://github.com',
'https://github.com/edit'
);
expect( manifest.categories[ 0 ].posts[ 0 ].edit_url ).toEqual(
'https://github.com/edit/example-docs/get-started/local-development.md'
);
} );
} ); } );

View File

@ -0,0 +1,41 @@
/**
* External dependencies
*/
import matter from 'gray-matter';
import yaml from 'js-yaml';
/**
* Generate front-matter for supported post attributes.
*
* @param fileContents
*/
export const generatePostFrontMatter = (
fileContents: string
): {
[ key: string ]: unknown;
} => {
const allowList = [
'post_date',
'post_title',
'page_template',
'post_author',
'post_name',
'category_title',
'category_slug',
];
const frontMatter = matter( fileContents, {
engines: {
// By passing yaml.JSON_SCHEMA we disable date parsing that changes date format.
// See https://github.com/jonschlinkert/gray-matter/issues/62#issuecomment-577628177 for more details.
yaml: ( s ) => yaml.load( s, { schema: yaml.JSON_SCHEMA } ),
},
} ).data;
return Object.keys( frontMatter )
.filter( ( key ) => allowList.includes( key ) )
.reduce( ( obj, key ) => {
obj[ key ] = frontMatter[ key ];
return obj;
}, {} );
};

View File

@ -3,10 +3,15 @@
*/ */
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import matter from 'gray-matter';
import { glob } from 'glob'; import { glob } from 'glob';
import crypto from 'crypto'; import crypto from 'crypto';
/**
* Internal dependencies
*/
import { generatePostFrontMatter } from './generate-frontmatter';
import { generateFileUrl } from './generate-urls';
interface Category { interface Category {
[ key: string ]: unknown; [ key: string ]: unknown;
posts?: Post[]; posts?: Post[];
@ -23,59 +28,28 @@ function generatePageId( filePath: string, prefix = '' ) {
return hash.digest( 'hex' ); 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( async function processDirectory(
rootDirectory: string, rootDirectory: string,
subDirectory: string, subDirectory: string,
projectName: string, projectName: string,
baseUrl: string, baseUrl: string,
baseEditUrl: string,
checkReadme = true checkReadme = true
): Promise< Category > { ): Promise< Category > {
let category: Category = {}; const category: Category = {};
// Process README.md (if exists) for the category definition. // Process README.md (if exists) for the category definition.
const readmePath = path.join( subDirectory, 'README.md' ); const readmePath = path.join( subDirectory, 'README.md' );
if ( checkReadme && fs.existsSync( readmePath ) ) { if ( checkReadme && fs.existsSync( readmePath ) ) {
const readmeContent = fs.readFileSync( readmePath, 'utf-8' ); const readmeContent = fs.readFileSync( readmePath, 'utf-8' );
const readmeFrontmatter = matter( readmeContent ).data; const frontMatter = generatePostFrontMatter( readmeContent );
category = { ...readmeFrontmatter }; Object.assign( category, frontMatter );
category.posts = []; } else if ( checkReadme ) {
// derive the category title from the directory name, capitalize first letter
const categoryTitle = path.basename( subDirectory );
category.category_title =
categoryTitle.charAt( 0 ).toUpperCase() + categoryTitle.slice( 1 );
} }
const markdownFiles = glob.sync( path.join( subDirectory, '*.md' ) ); const markdownFiles = glob.sync( path.join( subDirectory, '*.md' ) );
@ -84,7 +58,18 @@ async function processDirectory(
if ( filePath !== readmePath || ! checkReadme ) { if ( filePath !== readmePath || ! checkReadme ) {
// Skip README.md which we have already processed. // Skip README.md which we have already processed.
const fileContent = fs.readFileSync( filePath, 'utf-8' ); const fileContent = fs.readFileSync( filePath, 'utf-8' );
const fileFrontmatter = matter( fileContent ).data; const fileFrontmatter = generatePostFrontMatter( fileContent );
category.posts = [];
if ( baseUrl.includes( 'github' ) ) {
fileFrontmatter.edit_url = generateFileUrl(
baseEditUrl,
rootDirectory,
subDirectory,
filePath
);
}
const post: Post = { ...fileFrontmatter }; const post: Post = { ...fileFrontmatter };
category.posts.push( { category.posts.push( {
@ -111,7 +96,8 @@ async function processDirectory(
rootDirectory, rootDirectory,
subdirectory, subdirectory,
projectName, projectName,
baseUrl baseUrl,
baseEditUrl
); );
category.categories.push( subcategory ); category.categories.push( subcategory );
@ -124,13 +110,15 @@ export async function generateManifestFromDirectory(
rootDirectory: string, rootDirectory: string,
subDirectory: string, subDirectory: string,
projectName: string, projectName: string,
baseUrl: string baseUrl: string,
baseEditUrl: string
) { ) {
const manifest = await processDirectory( const manifest = await processDirectory(
rootDirectory, rootDirectory,
subDirectory, subDirectory,
projectName, projectName,
baseUrl, baseUrl,
baseEditUrl,
false false
); );

View File

@ -0,0 +1,40 @@
/**
* External dependencies
*/
import path from 'path';
/**
* 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 }`;
};