-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdot-md.php
More file actions
356 lines (304 loc) · 10.8 KB
/
dot-md.php
File metadata and controls
356 lines (304 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
<?php
/*
Plugin Name: Dot MD
Plugin URI: https://www.paidmembershipspro.com/
Description: Add .md to the end of a post URL to download a Markdown version. Makes it easy for AI to consume your content.
Version: 0.3.1
Author: Paid Memberships Pro
Author URI: https://www.paidmembershipspro.com
License: GPL2
*/
// Prevent direct access
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// Define plugin constants
define( 'DOTMD_VERSION', '0.3.1' );
define( 'DOTMD_DIR', dirname( __FILE__ ) );
define( 'DOTMD_URL', plugin_dir_url( __FILE__ ) );
// Load the HTML to Markdown library
require_once DOTMD_DIR . '/lib/HtmlConverterInterface.php';
require_once DOTMD_DIR . '/lib/HtmlConverter.php';
require_once DOTMD_DIR . '/lib/Configuration.php';
require_once DOTMD_DIR . '/lib/ConfigurationAwareInterface.php';
require_once DOTMD_DIR . '/lib/PreConverterInterface.php';
require_once DOTMD_DIR . '/lib/Environment.php';
require_once DOTMD_DIR . '/lib/ElementInterface.php';
require_once DOTMD_DIR . '/lib/Element.php';
require_once DOTMD_DIR . '/lib/Coerce.php';
// Load all converter classes
require_once DOTMD_DIR . '/lib/Converter/ConverterInterface.php';
require_once DOTMD_DIR . '/lib/Converter/DefaultConverter.php';
require_once DOTMD_DIR . '/lib/Converter/BlockquoteConverter.php';
require_once DOTMD_DIR . '/lib/Converter/CodeConverter.php';
require_once DOTMD_DIR . '/lib/Converter/CommentConverter.php';
require_once DOTMD_DIR . '/lib/Converter/DetailsConverter.php';
require_once DOTMD_DIR . '/lib/Converter/DivConverter.php';
require_once DOTMD_DIR . '/lib/Converter/EmphasisConverter.php';
require_once DOTMD_DIR . '/lib/Converter/HardBreakConverter.php';
require_once DOTMD_DIR . '/lib/Converter/HeaderConverter.php';
require_once DOTMD_DIR . '/lib/Converter/HorizontalRuleConverter.php';
require_once DOTMD_DIR . '/lib/Converter/IframeConverter.php';
require_once DOTMD_DIR . '/lib/Converter/ImageConverter.php';
require_once DOTMD_DIR . '/lib/Converter/InlineFormatConverter.php';
require_once DOTMD_DIR . '/lib/Converter/InputConverter.php';
require_once DOTMD_DIR . '/lib/Converter/LinkConverter.php';
require_once DOTMD_DIR . '/lib/Converter/ListBlockConverter.php';
require_once DOTMD_DIR . '/lib/Converter/ListItemConverter.php';
require_once DOTMD_DIR . '/lib/Converter/ParagraphConverter.php';
require_once DOTMD_DIR . '/lib/Converter/PreformattedConverter.php';
require_once DOTMD_DIR . '/lib/Converter/SemanticConverter.php';
require_once DOTMD_DIR . '/lib/Converter/TextConverter.php';
require_once DOTMD_DIR . '/lib/Converter/TableConverter.php';
/**
* Register the .md rewrite endpoint on activation
*/
function dotmd_activation() {
add_rewrite_endpoint( 'md', EP_PERMALINK | EP_PAGES );
dotmd_add_download_rewrite();
flush_rewrite_rules();
}
register_activation_hook( __FILE__, 'dotmd_activation' );
/**
* Initialize the .md endpoint
*/
function dotmd_init() {
add_rewrite_endpoint( 'md', EP_PERMALINK | EP_PAGES );
dotmd_add_download_rewrite();
}
add_action( 'init', 'dotmd_init' );
/**
* Add custom rewrite rule for .md download
* Supports URLs like: post-slug.md or page/path.md
*/
function dotmd_add_download_rewrite() {
// Match any URL ending in .md and route to WordPress with md=1 and download=1
// Using both 'name' (for posts) and 'pagename' (for pages) lets WordPress resolve correctly
add_rewrite_rule(
'^(.+?)\.md/?$',
'index.php?name=$matches[1]&pagename=$matches[1]&md=1&download=1',
'top'
);
}
/**
* Register the custom query var for download parameter
*/
function dotmd_query_vars( $vars ) {
$vars[] = 'download';
return $vars;
}
add_filter( 'query_vars', 'dotmd_query_vars' );
/**
* Remove the endpoint on deactivation
*/
function dotmd_deactivation() {
flush_rewrite_rules();
}
register_deactivation_hook( __FILE__, 'dotmd_deactivation' );
/**
* Detect .md endpoint and return markdown file
*/
function dotmd_template_redirect() {
global $wp_query, $post;
// Check if the md endpoint is set
if ( ! isset( $wp_query->query_vars['md'] ) ) {
return;
}
// Verify post exists
if ( empty( $post->ID ) ) {
return;
}
// Security: Block password-protected posts
if ( post_password_required( $post ) ) {
status_header( 403 );
wp_die( 'This content is password protected.', 'Password Protected', array( 'response' => 403 ) );
}
// Security: Block non-published posts (draft, private, pending, etc.)
if ( $post->post_status !== 'publish' ) {
status_header( 404 );
wp_die( 'Not found.', 'Not Found', array( 'response' => 404 ) );
}
// Security: Block non-public post types
$post_type_object = get_post_type_object( $post->post_type );
if ( empty( $post_type_object ) || ! $post_type_object->public ) {
status_header( 404 );
wp_die( 'Not found.', 'Not Found', array( 'response' => 404 ) );
}
// Check if download parameter is set (from .htaccess rewrite)
$is_download = isset( $wp_query->query_vars['download'] ) && $wp_query->query_vars['download'] == '1';
// Only cache for logged-out users to prevent leaking personalized content
$is_logged_out = ! is_user_logged_in();
$markdown = false;
if ( $is_logged_out ) {
$post_version = get_post_meta( $post->ID, '_dotmd_version', true );
if ( empty( $post_version ) ) {
$post_version = 1;
update_post_meta( $post->ID, '_dotmd_version', $post_version );
}
$cache_key = 'dotmd_' . $post->ID . '_public_pv' . $post_version . '_v' . DOTMD_VERSION;
$markdown = get_transient( $cache_key );
}
// If no cache or logged in, generate markdown
if ( false === $markdown ) {
$markdown = dotmd_generate_markdown( $post );
// Cache only for logged-out users (personalized content should not be cached)
if ( $is_logged_out ) {
set_transient( $cache_key, $markdown, WEEK_IN_SECONDS );
}
}
// Set headers - download if download=1 parameter, display otherwise
header( 'Content-Type: text/plain; charset=utf-8' );
if ( $is_download ) {
// .md extension triggers download
header( 'Content-Disposition: attachment; filename="' . $post->post_name . '.md"' );
} else {
// /md/ endpoint displays in browser
header( 'Content-Disposition: inline; filename="' . $post->post_name . '.md"' );
}
// Security headers
header( 'X-Content-Type-Options: nosniff' );
header( 'Vary: Cookie' ); // Output may differ based on authentication
// Allow filtering the final output before sending to client
$markdown = apply_filters( 'dotmd_pre_output', $markdown, $post );
// Output markdown
echo $markdown;
exit;
}
add_action( 'template_redirect', 'dotmd_template_redirect' );
/**
* Clean up extra whitespace in markdown output
*
* @param string $markdown The markdown content
* @return string The cleaned markdown
*/
function dotmd_cleanup_whitespace( $markdown ) {
// Split into lines
$lines = explode( "\n", $markdown );
// Remove leading whitespace from each line (except code blocks)
$in_code_block = false;
$cleaned_lines = array();
foreach ( $lines as $line ) {
// Detect code block boundaries
if ( strpos( $line, '```' ) === 0 ) {
$in_code_block = ! $in_code_block;
$cleaned_lines[] = $line;
continue;
}
// Don't trim whitespace inside code blocks
if ( $in_code_block ) {
$cleaned_lines[] = $line;
continue;
}
// Trim leading whitespace from non-code lines
$cleaned_lines[] = ltrim( $line );
}
// Join back together
$markdown = implode( "\n", $cleaned_lines );
// Remove excessive blank lines (more than 2 consecutive)
$markdown = preg_replace( "/\n{3,}/", "\n\n", $markdown );
return $markdown;
}
/**
* Generate markdown from post content
*
* @param WP_Post $post The post object
* @return string The markdown content
*/
function dotmd_generate_markdown( $post ) {
// Get the rendered post content with all filters applied
$content = apply_filters( 'the_content', $post->post_content );
// Initialize the HTML to Markdown converter
$converter = new \DotMD\HtmlToMarkdown\HtmlConverter( array(
'header_style' => 'atx',
'strip_tags' => false,
'remove_nodes' => 'script style',
'hard_break' => true,
'list_item_style' => '-',
) );
// Build the complete markdown document
$markdown = '';
// Add title
$markdown .= '# ' . $post->post_title . "\n\n";
// Add metadata
$markdown .= '_Published: ' . get_the_date( 'F j, Y', $post ) . '_' . "\n\n";
$markdown .= '_Author: ' . get_the_author_meta( 'display_name', $post->post_author ) . '_' . "\n\n";
$markdown .= '_URL: ' . get_permalink( $post->ID ) . '_' . "\n\n";
// Add excerpt if available
if ( ! empty( $post->post_excerpt ) ) {
$markdown .= '**Excerpt:** ' . $post->post_excerpt . "\n\n";
}
// Add horizontal rule
$markdown .= "---\n\n";
// Convert content to markdown
try {
$content_markdown = $converter->convert( $content );
// Clean up extra whitespace
$content_markdown = dotmd_cleanup_whitespace( $content_markdown );
$markdown .= $content_markdown;
} catch ( Exception $e ) {
// If conversion fails, fall back to plain text (structure will be lost)
$markdown .= "<!-- Warning: Markdown conversion failed, showing plain text -->\n\n";
$markdown .= strip_tags( $content );
}
// Add footer
$markdown .= "\n\n---\n\n";
$markdown .= '_Generated from: ' . get_permalink( $post->ID ) . '_' . "\n";
// Allow filtering the final markdown
return apply_filters( 'dotmd_markdown_output', $markdown, $post );
}
/**
* Clear markdown cache when post is updated
* Increments the post version to invalidate all user-specific caches
*
* @param int $post_id The post ID
*/
function dotmd_clear_cache( $post_id ) {
// Don't clear cache for autosaves or revisions
if ( wp_is_post_autosave( $post_id ) || wp_is_post_revision( $post_id ) ) {
return;
}
// Increment the post version to invalidate all cached versions for all users
$post_version = get_post_meta( $post_id, '_dotmd_version', true );
if ( empty( $post_version ) ) {
$post_version = 1;
} else {
$post_version++;
}
update_post_meta( $post_id, '_dotmd_version', $post_version );
}
add_action( 'save_post', 'dotmd_clear_cache' );
add_action( 'delete_post', 'dotmd_clear_cache' );
/**
* Add a link to the admin bar for easy access to markdown version
*
* @param WP_Admin_Bar $wp_admin_bar
*/
function dotmd_admin_bar_link( $wp_admin_bar ) {
// Only show on singular posts/pages
if ( ! is_singular() ) {
return;
}
global $post;
// Don't show link if post is password-protected
if ( post_password_required( $post ) ) {
return;
}
// Don't show link for non-published posts
if ( $post->post_status !== 'publish' ) {
return;
}
// Don't show link for non-public post types
$post_type_object = get_post_type_object( $post->post_type );
if ( empty( $post_type_object ) || ! $post_type_object->public ) {
return;
}
$wp_admin_bar->add_node( array(
'id' => 'dotmd-download',
'title' => 'Download as Markdown',
'href' => trailingslashit( get_permalink( $post->ID ) ) . 'md/',
'meta' => array(
'title' => 'Download this post as a Markdown file',
),
) );
}
add_action( 'admin_bar_menu', 'dotmd_admin_bar_link', 100 );