From a9202bb4bc635c66a07996873ab2d43148719416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Thu, 2 Jan 2025 01:33:55 +0100 Subject: [PATCH] Rewrite WordPress URLs to relative URLs when serializing to markdown --- .../src/WP_Blocks_To_Markdown.php | 7 ++ .../plugin.php | 61 +++++++++++++++-- .../ui/src/index.tsx | 67 +++++++++---------- .../WP_Block_Markup_Url_Processor.php | 3 + 4 files changed, 95 insertions(+), 43 deletions(-) diff --git a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php index 95206f36d7..276ba1f754 100644 --- a/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php +++ b/packages/playground/data-liberation-markdown/src/WP_Blocks_To_Markdown.php @@ -75,6 +75,13 @@ private function block_to_markdown($block) { return "{$fence}{$language}\n{$code}\n{$fence}\n\n"; case 'core/image': + if(!isset($attributes['url'])) { + $processor = WP_Data_Liberation_HTML_Processor::create_fragment($inner_html); + if($processor->next_tag('img')) { + $attributes['url'] = $processor->get_attribute('src'); + $attributes['alt'] = $processor->get_attribute('alt'); + } + } return "![" . ($attributes['alt'] ?? '') . "](" . ($attributes['url'] ?? '') . ")\n\n"; case 'core/heading': diff --git a/packages/playground/data-liberation-static-files-editor/plugin.php b/packages/playground/data-liberation-static-files-editor/plugin.php index 657f41b486..d6f318c82a 100644 --- a/packages/playground/data-liberation-static-files-editor/plugin.php +++ b/packages/playground/data-liberation-static-files-editor/plugin.php @@ -23,8 +23,12 @@ use WordPress\Filesystem\WP_Filesystem_Visitor; use WordPress\Filesystem\WP_Uploaded_Directory_Tree_Filesystem; -if ( ! defined( 'WP_STATIC_CONTENT_DIR' ) ) { - define( 'WP_STATIC_CONTENT_DIR', WP_CONTENT_DIR . '/uploads/static-pages' ); +if ( ! defined( 'WP_STATIC_PAGES_DIR' ) ) { + define( 'WP_STATIC_PAGES_DIR', WP_CONTENT_DIR . '/uploads/static-pages' ); +} + +if ( ! defined( 'WP_STATIC_MEDIA_DIR' ) ) { + define( 'WP_STATIC_MEDIA_DIR', WP_STATIC_PAGES_DIR . '/media' ); } if( ! defined( 'WP_LOCAL_FILE_POST_TYPE' )) { @@ -52,11 +56,10 @@ class WP_Static_Files_Editor_Plugin { static private function get_fs() { if(!self::$fs) { - $dot_git_path = WP_CONTENT_DIR . '/.static-pages.git'; - if(!is_dir($dot_git_path)) { - mkdir($dot_git_path, 0777, true); + if(!is_dir(WP_STATIC_PAGES_DIR)) { + mkdir(WP_STATIC_PAGES_DIR, 0777, true); } - $local_fs = new WP_Local_Filesystem($dot_git_path); + $local_fs = new WP_Local_Filesystem(WP_STATIC_PAGES_DIR); $repo = new WP_Git_Repository($local_fs); $repo->add_remote('origin', GIT_REPO_URL); $repo->set_ref_head('HEAD', 'refs/heads/' . GIT_BRANCH); @@ -517,6 +520,8 @@ static private function convert_post_to_string($path, $post) { // ones explicitly set by the user in the editor? $content = get_post_field('post_content', $post_id); + $content = self::unwordpressify_static_assets_urls($content); + switch($extension) { // @TODO: Add support for HTML and XHTML case 'html': @@ -530,6 +535,48 @@ static private function convert_post_to_string($path, $post) { return $converter->get_result(); } + /** + * Convert references to files served via download_file_endpoint + * to an absolute path referring to the corresponding static files + * in the local filesystem. + */ + static private function unwordpressify_static_assets_urls($content) { + $site_url = WP_URL::parse(get_site_url()); + $expected_endpoint_path = '/wp-json/static-files-editor/v1/download-file'; + $p = WP_Block_Markup_Url_Processor::create_from_html($content, $site_url); + while($p->next_url()) { + $url = $p->get_parsed_url(); + if(!is_child_url_of($url, get_site_url())) { + continue; + } + + // Account for sites with no nice permalink structure + if($url->searchParams->has('rest_route')) { + $url = WP_URL::parse($url->searchParams->get('rest_route'), $site_url); + } + + // Naively check for the endpoint that serves the file. + // WordPress can use a custom REST API prefix which this + // check doesn't account for. It assumes the endpoint path + // is unique enough to not conflict with other paths. + // + // It may need to be revisited if any conflicts arise in + // the future. + if(!str_ends_with($url->pathname, $expected_endpoint_path)) { + continue; + } + + // At this point we're certain the URL intends to download + // a static file managed by this plugin. + + // Let's replace the URL in the content with the relative URL. + $original_url = $url->searchParams->get('path'); + $p->set_raw_url($original_url); + } + + return $p->get_updated_html(); + } + static public function get_local_files_tree($subdirectory = '') { $tree = []; $fs = self::get_fs(); @@ -608,7 +655,7 @@ static private function build_local_file_tree_recursive($fs, $dir, &$tree, $path * @TODO: Error handling */ static public function import_static_pages() { - if ( ! is_dir( WP_STATIC_CONTENT_DIR ) ) { + if ( ! is_dir( WP_STATIC_PAGES_DIR ) ) { return; } diff --git a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx index 8429f50d7d..f15503daaa 100644 --- a/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx +++ b/packages/playground/data-liberation-static-files-editor/ui/src/index.tsx @@ -13,11 +13,10 @@ import { import apiFetch from '@wordpress/api-fetch'; import { addComponentToEditorContentArea, - addLoadingOverlay, addLocalFilesTab, } from './add-local-files-tab'; import { store as blockEditorStore } from '@wordpress/block-editor'; -import { Spinner, Button } from '@wordpress/components'; +import { Spinner } from '@wordpress/components'; import { useEntityProp, store as coreStore } from '@wordpress/core-data'; import css from './style.module.css'; import { FileTree } from 'components/FilePickerTree/types'; @@ -79,42 +78,39 @@ function ConnectedFilePickerTree() { // Get the current post's file path from meta const [meta] = useEntityProp('postType', WP_LOCAL_FILE_POST_TYPE, 'meta'); - const [selectedPath, setSelectedPath] = useState( - meta?.local_file_path || '/' + + const initialPostId = useSelect( + (select) => select(editorStore).getCurrentPostId(), + [] ); + const [selectedNode, setSelectedNode] = useState({ + path: meta?.local_file_path || '/', + postId: initialPostId, + type: 'folder' as 'file' | 'folder', + }); + useEffect(() => { async function refreshPostId() { - console.log( - 'refreshPostId', - selectedPath, - isStaticAssetPath(selectedPath) - ); - if (isStaticAssetPath(selectedPath)) { + if (isStaticAssetPath(selectedNode.path)) { setPostLoading(false); - setSelectedPostId(null); - setPreviewPath(selectedPath); - } else { + setSelectedNode((prev) => ({ ...prev, postId: null })); + setPreviewPath(selectedNode.path); + } else if (selectedNode.type === 'file') { setPostLoading(true); - if (!selectedPostId) { + if (!selectedNode.postId) { const { post_id } = (await apiFetch({ path: '/static-files-editor/v1/get-or-create-post-for-file', method: 'POST', - data: { path: selectedPath }, + data: { path: selectedNode.path }, })) as { post_id: string }; - setSelectedPostId(post_id); + setSelectedNode((prev) => ({ ...prev, postId: post_id })); } setPreviewPath(null); } } refreshPostId(); - }, [selectedPath]); - - const initialPostId = useSelect( - (select) => select(editorStore).getCurrentPostId(), - [] - ); - const [selectedPostId, setSelectedPostId] = useState(initialPostId); + }, [selectedNode.path]); const { post, hasLoadedPost, onNavigateToEntityRecord } = useSelect( (select) => { @@ -127,16 +123,16 @@ function ConnectedFilePickerTree() { post: getEntityRecord( 'postType', WP_LOCAL_FILE_POST_TYPE, - selectedPostId + selectedNode.postId ), hasLoadedPost: hasFinishedResolution('getEntityRecord', [ 'postType', WP_LOCAL_FILE_POST_TYPE, - selectedPostId, + selectedNode.postId, ]), }; }, - [selectedPostId] + [selectedNode.postId] ); const { setPostLoading, setPreviewPath } = useDispatch(STORE_NAME); @@ -145,16 +141,16 @@ function ConnectedFilePickerTree() { // Only navigate once the post has been loaded. Otherwise the editor // will disappear for a second – the component renders its // children conditionally on having the post available. - if (selectedPostId) { + if (selectedNode.postId) { setPostLoading(!hasLoadedPost); if (hasLoadedPost && post) { onNavigateToEntityRecord({ - postId: selectedPostId, + postId: selectedNode.postId, postType: WP_LOCAL_FILE_POST_TYPE, }); } } - }, [hasLoadedPost, post, setPostLoading, selectedPostId]); + }, [hasLoadedPost, post, setPostLoading, selectedNode.postId]); const refreshFileTree = useCallback(async () => { fileTreePromise = apiFetch({ @@ -190,12 +186,11 @@ function ConnectedFilePickerTree() { }; const handleFileClick = async (filePath: string, node: FileNode) => { - setSelectedPath(filePath); - if (node.post_id && !isStaticAssetPath(filePath)) { - setSelectedPostId(node.post_id); - } else { - setSelectedPostId(null); - } + setSelectedNode({ + path: filePath, + postId: node.post_id, + type: node.type, + }); }; const handleNodesCreated = async (tree: FileTree) => { @@ -305,7 +300,7 @@ function ConnectedFilePickerTree() { base_url_string = $base_url_string;