Changeset 3454428
- Timestamp:
- 02/05/2026 09:46:05 AM (2 weeks ago)
- Location:
- djot-markup
- Files:
-
- 4 added
- 40 edited
- 1 copied
-
tags/1.4.0 (copied) (copied from djot-markup/trunk)
-
tags/1.4.0/assets/blocks/djot/block.json (modified) (1 diff)
-
tags/1.4.0/assets/blocks/djot/index.asset.php (modified) (1 diff)
-
tags/1.4.0/assets/css/djot.css (modified) (2 diffs)
-
tags/1.4.0/composer.json (modified) (1 diff)
-
tags/1.4.0/readme.txt (modified) (4 diffs)
-
tags/1.4.0/src/Admin/Settings.php (modified) (5 diffs)
-
tags/1.4.0/src/Converter.php (modified) (8 diffs)
-
tags/1.4.0/src/Plugin.php (modified) (2 diffs)
-
tags/1.4.0/vendor/autoload.php (modified) (1 diff)
-
tags/1.4.0/vendor/composer/autoload_classmap.php (modified) (3 diffs)
-
tags/1.4.0/vendor/composer/autoload_real.php (modified) (2 diffs)
-
tags/1.4.0/vendor/composer/autoload_static.php (modified) (5 diffs)
-
tags/1.4.0/vendor/composer/installed.json (modified) (3 diffs)
-
tags/1.4.0/vendor/composer/installed.php (modified) (3 diffs)
-
tags/1.4.0/vendor/php-collective/djot/src/DjotConverter.php (modified) (2 diffs)
-
tags/1.4.0/vendor/php-collective/djot/src/Extension/HeadingPermalinksExtension.php (modified) (8 diffs)
-
tags/1.4.0/vendor/php-collective/djot/src/Extension/SmartQuotesExtension.php (added)
-
tags/1.4.0/vendor/php-collective/djot/src/Extension/TableOfContentsExtension.php (modified) (4 diffs)
-
tags/1.4.0/vendor/php-collective/djot/src/Parser/InlineParser.php (modified) (8 diffs)
-
tags/1.4.0/vendor/php-collective/djot/src/Renderer/HeadingIdTracker.php (added)
-
tags/1.4.0/vendor/php-collective/djot/src/Renderer/HtmlRenderer.php (modified) (5 diffs)
-
tags/1.4.0/wp-djot.php (modified) (2 diffs)
-
trunk/assets/blocks/djot/block.json (modified) (1 diff)
-
trunk/assets/blocks/djot/index.asset.php (modified) (1 diff)
-
trunk/assets/css/djot.css (modified) (2 diffs)
-
trunk/composer.json (modified) (1 diff)
-
trunk/readme.txt (modified) (4 diffs)
-
trunk/src/Admin/Settings.php (modified) (5 diffs)
-
trunk/src/Converter.php (modified) (8 diffs)
-
trunk/src/Plugin.php (modified) (2 diffs)
-
trunk/vendor/autoload.php (modified) (1 diff)
-
trunk/vendor/composer/autoload_classmap.php (modified) (3 diffs)
-
trunk/vendor/composer/autoload_real.php (modified) (2 diffs)
-
trunk/vendor/composer/autoload_static.php (modified) (5 diffs)
-
trunk/vendor/composer/installed.json (modified) (3 diffs)
-
trunk/vendor/composer/installed.php (modified) (3 diffs)
-
trunk/vendor/php-collective/djot/src/DjotConverter.php (modified) (2 diffs)
-
trunk/vendor/php-collective/djot/src/Extension/HeadingPermalinksExtension.php (modified) (8 diffs)
-
trunk/vendor/php-collective/djot/src/Extension/SmartQuotesExtension.php (added)
-
trunk/vendor/php-collective/djot/src/Extension/TableOfContentsExtension.php (modified) (4 diffs)
-
trunk/vendor/php-collective/djot/src/Parser/InlineParser.php (modified) (8 diffs)
-
trunk/vendor/php-collective/djot/src/Renderer/HeadingIdTracker.php (added)
-
trunk/vendor/php-collective/djot/src/Renderer/HtmlRenderer.php (modified) (5 diffs)
-
trunk/wp-djot.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
djot-markup/tags/1.4.0/assets/blocks/djot/block.json
r3454148 r3454428 3 3 "apiVersion": 3, 4 4 "name": "wpdjot/djot", 5 "version": "1. 3.1",5 "version": "1.4.0", 6 6 "title": "Djot", 7 7 "category": "text", -
djot-markup/tags/1.4.0/assets/blocks/djot/index.asset.php
r3454148 r3454428 10 10 'wp-api-fetch', 11 11 ], 12 'version' => '1. 3.1',12 'version' => '1.4.0', 13 13 ]; -
djot-markup/tags/1.4.0/assets/css/djot.css
r3454148 r3454428 279 279 text-align: center; 280 280 font-style: italic; 281 } 282 283 /* Table of Contents */ 284 .wpdjot-toc { 285 background: #f8f9fa; 286 border: 1px solid #e9ecef; 287 border-radius: 6px; 288 padding: 0; 289 margin-bottom: 1.5em; 290 } 291 292 .wpdjot-toc summary { 293 padding: 0.75em 1.5em; 294 font-weight: 600; 295 cursor: pointer; 296 list-style: none; 297 display: flex; 298 align-items: center; 299 gap: 0.5em; 300 } 301 302 .wpdjot-toc summary::before { 303 content: "▶"; 304 font-size: 0.7em; 305 transition: transform 0.2s ease; 306 } 307 308 .wpdjot-toc[open] summary::before { 309 transform: rotate(90deg); 310 } 311 312 .wpdjot-toc summary::-webkit-details-marker { 313 display: none; 314 } 315 316 .wpdjot-toc > ul, 317 .wpdjot-toc > ol { 318 margin: 0; 319 padding: 0 1.5em 1em; 320 } 321 322 .wpdjot-toc ul, 323 .wpdjot-toc ol { 324 margin: 0; 325 padding-left: 1.5em; 326 } 327 328 .wpdjot-toc li { 329 margin: 0.25em 0; 330 line-height: 1.4; 331 } 332 333 .wpdjot-toc a { 334 color: inherit; 335 text-decoration: none; 336 } 337 338 .wpdjot-toc a:hover { 339 text-decoration: underline; 340 color: #0066cc; 341 } 342 343 /* Heading Permalinks */ 344 .djot-content .permalink-wrapper { 345 opacity: 0; 346 transition: opacity 0.2s ease; 347 margin-left: 0.3em; 348 } 349 350 .djot-content section:hover > h1 .permalink-wrapper, 351 .djot-content section:hover > h2 .permalink-wrapper, 352 .djot-content section:hover > h3 .permalink-wrapper, 353 .djot-content section:hover > h4 .permalink-wrapper, 354 .djot-content section:hover > h5 .permalink-wrapper, 355 .djot-content section:hover > h6 .permalink-wrapper { 356 opacity: 1; 357 } 358 359 .djot-content .wpdjot-permalink { 360 color: #57606a; 361 text-decoration: none; 362 font-weight: 400; 363 cursor: pointer; 364 } 365 366 .djot-content .wpdjot-permalink:hover { 367 color: #0969da; 368 text-decoration: none; 281 369 } 282 370 … … 690 778 color: #8b949e; 691 779 } 692 } 780 781 .wpdjot-toc { 782 background: #161b22; 783 border-color: #30363d; 784 } 785 786 .wpdjot-toc a:hover { 787 color: #58a6ff; 788 } 789 790 .djot-content .wpdjot-permalink { 791 color: #8b949e; 792 } 793 794 .djot-content .wpdjot-permalink:hover { 795 color: #58a6ff; 796 } 797 } -
djot-markup/tags/1.4.0/composer.json
r3454148 r3454428 12 12 "require": { 13 13 "php": ">=8.2", 14 "php-collective/djot": "^0.1.1 1"14 "php-collective/djot": "^0.1.13" 15 15 }, 16 16 "require-dev": { -
djot-markup/tags/1.4.0/readme.txt
r3454148 r3454428 5 5 Tested up to: 6.9 6 6 Requires PHP: 8.2 7 Stable tag: 1. 3.17 Stable tag: 1.4.0 8 8 License: MIT 9 9 License URI: https://opensource.org/licenses/MIT … … 20 20 * **Shortcode Support**: Use `[djot]...[/djot]` in your content 21 21 * **Content Filtering**: Automatically process `{djot}...{/djot}` blocks 22 * **Table of Contents**: Automatic TOC generation from headings with configurable levels and position 22 23 * **Safe Mode**: XSS protection for untrusted content 23 24 * **Syntax Highlighting**: Built-in highlight.js integration with 12+ themes … … 90 91 == Changelog == 91 92 93 = 1.4.0 = 94 * Added automatic Table of Contents generation from headings 95 * Configurable TOC position (top/bottom), heading levels, and list type 96 * Light and dark mode styling for TOC 97 * Added heading permalinks with show-on-hover and copy-to-clipboard 98 * Added locale-aware smart quotes (20 locales + Auto from site language) 99 * Bumped php-collective/djot to ^0.1.13 100 92 101 = 1.3.1 = 93 102 * Fixed text domain to match plugin slug (djot-markup) … … 128 137 == Upgrade Notice == 129 138 139 = 1.4.0 = 140 Adds Table of Contents, heading permalinks, and locale-aware smart quotes. 141 130 142 = 1.3.1 = 131 143 Text domain, escaping, and Plugin Check fixes for WordPress.org plugin review compliance. -
djot-markup/tags/1.4.0/src/Admin/Settings.php
r3454148 r3454428 9 9 exit; 10 10 } 11 12 use Djot\Extension\SmartQuotesExtension; 11 13 12 14 /** … … 206 208 ); 207 209 210 add_settings_field( 211 'smart_quotes_locale', 212 __('Smart Quotes', 'djot-markup'), 213 [$this, 'renderSmartQuotesLocaleSelect'], 214 self::PAGE_SLUG, 215 'wpdjot_rendering', 216 ['field' => 'smart_quotes_locale', 'description' => __('Choose which typographic quote characters to use. Default English uses curly quotes.', 'djot-markup')], 217 ); 218 208 219 // Code Highlighting Section 209 220 add_settings_section( … … 247 258 'wpdjot_advanced', 248 259 ['field' => 'shortcode_tag', 'description' => __('The shortcode tag to use (default: djot).', 'djot-markup')], 260 ); 261 262 // Table of Contents Section 263 add_settings_section( 264 'wpdjot_toc', 265 __('Table of Contents', 'djot-markup'), 266 [$this, 'renderTocSectionDescription'], 267 self::PAGE_SLUG, 268 ); 269 270 add_settings_field( 271 'toc_enabled', 272 __('Enable TOC', 'djot-markup'), 273 [$this, 'renderCheckboxField'], 274 self::PAGE_SLUG, 275 'wpdjot_toc', 276 ['field' => 'toc_enabled', 'description' => __('Automatically generate a table of contents from headings in posts and pages.', 'djot-markup')], 277 ); 278 279 add_settings_field( 280 'toc_position', 281 __('TOC Position', 'djot-markup'), 282 [$this, 'renderTocPositionSelect'], 283 self::PAGE_SLUG, 284 'wpdjot_toc', 285 ['field' => 'toc_position', 'description' => __('Where to insert the table of contents.', 'djot-markup')], 286 ); 287 288 add_settings_field( 289 'toc_min_level', 290 __('Minimum Heading Level', 'djot-markup'), 291 [$this, 'renderHeadingLevelSelect'], 292 self::PAGE_SLUG, 293 'wpdjot_toc', 294 ['field' => 'toc_min_level', 'description' => __('Include headings starting from this level (default: 2 to skip page title).', 'djot-markup')], 295 ); 296 297 add_settings_field( 298 'toc_max_level', 299 __('Maximum Heading Level', 'djot-markup'), 300 [$this, 'renderHeadingLevelSelect'], 301 self::PAGE_SLUG, 302 'wpdjot_toc', 303 ['field' => 'toc_max_level', 'description' => __('Include headings up to this level.', 'djot-markup')], 304 ); 305 306 add_settings_field( 307 'toc_list_type', 308 __('TOC List Type', 'djot-markup'), 309 [$this, 'renderTocListTypeSelect'], 310 self::PAGE_SLUG, 311 'wpdjot_toc', 312 ['field' => 'toc_list_type', 'description' => __('Use ordered (numbered) or unordered (bulleted) list.', 'djot-markup')], 313 ); 314 315 add_settings_field( 316 'permalinks_enabled', 317 __('Heading Permalinks', 'djot-markup'), 318 [$this, 'renderCheckboxField'], 319 self::PAGE_SLUG, 320 'wpdjot_toc', 321 ['field' => 'permalinks_enabled', 'description' => __('Add clickable # symbols to headings. Shown on hover, clicking copies the heading URL to clipboard.', 'djot-markup')], 249 322 ); 250 323 } … … 287 360 ? $input['comment_soft_break'] 288 361 : 'newline', 362 'smart_quotes_locale' => in_array($input['smart_quotes_locale'] ?? '', array_merge(['auto'], SmartQuotesExtension::getSupportedLocales()), true) 363 ? $input['smart_quotes_locale'] 364 : 'en', 365 'toc_enabled' => !empty($input['toc_enabled']), 366 'toc_position' => in_array($input['toc_position'] ?? '', ['top', 'bottom'], true) 367 ? $input['toc_position'] 368 : 'top', 369 'toc_min_level' => in_array((int)($input['toc_min_level'] ?? 2), [1, 2, 3, 4, 5, 6], true) 370 ? (int)$input['toc_min_level'] 371 : 2, 372 'toc_max_level' => in_array((int)($input['toc_max_level'] ?? 4), [1, 2, 3, 4, 5, 6], true) 373 ? (int)$input['toc_max_level'] 374 : 4, 375 'toc_list_type' => in_array($input['toc_list_type'] ?? '', ['ul', 'ol'], true) 376 ? $input['toc_list_type'] 377 : 'ul', 378 'permalinks_enabled' => !empty($input['permalinks_enabled']), 289 379 ]; 290 380 } … … 558 648 echo '</ul>'; 559 649 } 650 651 /** 652 * Render smart quotes locale select dropdown. 653 * 654 * @param array<string, mixed> $args 655 */ 656 public function renderSmartQuotesLocaleSelect(array $args): void 657 { 658 $options = get_option(self::OPTION_GROUP, []); 659 $field = $args['field']; 660 $current = $options[$field] ?? 'en'; 661 662 $localeLabels = [ 663 'en' => "Default (English \u{201C}\u{2026}\u{201D} \u{2018}\u{2026}\u{2019})", 664 'auto' => 'Auto (from site language)', 665 'cs' => "Czech \u{201E}\u{2026}\u{201C} \u{201A}\u{2026}\u{2018}", 666 'da' => "Danish \u{201E}\u{2026}\u{201C} \u{201A}\u{2026}\u{2018}", 667 'nl' => "Dutch \u{201C}\u{2026}\u{201D} \u{2018}\u{2026}\u{2019}", 668 'fi' => "Finnish \u{201D}\u{2026}\u{201D} \u{2019}\u{2026}\u{2019}", 669 'fr' => "French \u{00AB}\u{2026}\u{00BB} \u{2039}\u{2026}\u{203A}", 670 'de' => "German \u{201E}\u{2026}\u{201C} \u{201A}\u{2026}\u{2018}", 671 'de-CH' => "Swiss German \u{00AB}\u{2026}\u{00BB} \u{2039}\u{2026}\u{203A}", 672 'hu' => "Hungarian \u{201E}\u{2026}\u{201D} \u{201A}\u{2026}\u{2019}", 673 'it' => "Italian \u{00AB}\u{2026}\u{00BB} \u{201C}\u{2026}\u{201D}", 674 'ja' => "Japanese \u{300C}\u{2026}\u{300D} \u{300E}\u{2026}\u{300F}", 675 'nb' => "Norwegian Bokm\u{00E5}l \u{00AB}\u{2026}\u{00BB} \u{2018}\u{2026}\u{2019}", 676 'nn' => "Norwegian Nynorsk \u{00AB}\u{2026}\u{00BB} \u{2018}\u{2026}\u{2019}", 677 'pl' => "Polish \u{201E}\u{2026}\u{201D} \u{201A}\u{2026}\u{2019}", 678 'pt' => "Portuguese \u{00AB}\u{2026}\u{00BB} \u{201C}\u{2026}\u{201D}", 679 'ru' => "Russian \u{00AB}\u{2026}\u{00BB} \u{201E}\u{2026}\u{201C}", 680 'es' => "Spanish \u{00AB}\u{2026}\u{00BB} \u{201C}\u{2026}\u{201D}", 681 'sv' => "Swedish \u{201D}\u{2026}\u{201D} \u{2019}\u{2026}\u{2019}", 682 'uk' => "Ukrainian \u{00AB}\u{2026}\u{00BB} \u{201E}\u{2026}\u{201C}", 683 'zh' => "Chinese \u{300C}\u{2026}\u{300D} \u{300E}\u{2026}\u{300F}", 684 ]; 685 686 printf( 687 '<select id="%1$s" name="%2$s[%1$s]">', 688 esc_attr($field), 689 esc_attr(self::OPTION_GROUP), 690 ); 691 692 foreach ($localeLabels as $value => $label) { 693 printf( 694 '<option value="%s" %s>%s</option>', 695 esc_attr($value), 696 selected($current, $value, false), 697 esc_html($label), 698 ); 699 } 700 701 echo '</select>'; 702 703 if (!empty($args['description'])) { 704 printf('<p class="description">%s</p>', esc_html($args['description'])); 705 } 706 } 707 708 public function renderTocSectionDescription(): void 709 { 710 echo '<p>' . esc_html__('Configure automatic table of contents generation for posts and pages.', 'djot-markup') . '</p>'; 711 } 712 713 /** 714 * Render TOC position select dropdown. 715 * 716 * @param array<string, mixed> $args 717 */ 718 public function renderTocPositionSelect(array $args): void 719 { 720 $options = get_option(self::OPTION_GROUP, []); 721 $field = $args['field']; 722 $current = $options[$field] ?? 'top'; 723 724 $positions = [ 725 'top' => __('Top of content', 'djot-markup'), 726 'bottom' => __('Bottom of content', 'djot-markup'), 727 ]; 728 729 printf( 730 '<select id="%1$s" name="%2$s[%1$s]">', 731 esc_attr($field), 732 esc_attr(self::OPTION_GROUP), 733 ); 734 735 foreach ($positions as $value => $label) { 736 printf( 737 '<option value="%s" %s>%s</option>', 738 esc_attr($value), 739 selected($current, $value, false), 740 esc_html($label), 741 ); 742 } 743 744 echo '</select>'; 745 746 if (!empty($args['description'])) { 747 printf('<p class="description">%s</p>', esc_html($args['description'])); 748 } 749 } 750 751 /** 752 * Render heading level select dropdown. 753 * 754 * @param array<string, mixed> $args 755 */ 756 public function renderHeadingLevelSelect(array $args): void 757 { 758 $options = get_option(self::OPTION_GROUP, []); 759 $field = $args['field']; 760 $default = $field === 'toc_min_level' ? 2 : 4; 761 $current = $options[$field] ?? $default; 762 763 printf( 764 '<select id="%1$s" name="%2$s[%1$s]">', 765 esc_attr($field), 766 esc_attr(self::OPTION_GROUP), 767 ); 768 769 for ($level = 1; $level <= 6; $level++) { 770 printf( 771 '<option value="%d" %s>H%d</option>', 772 $level, 773 selected((int)$current, $level, false), 774 $level, 775 ); 776 } 777 778 echo '</select>'; 779 780 if (!empty($args['description'])) { 781 printf('<p class="description">%s</p>', esc_html($args['description'])); 782 } 783 } 784 785 /** 786 * Render TOC list type select dropdown. 787 * 788 * @param array<string, mixed> $args 789 */ 790 public function renderTocListTypeSelect(array $args): void 791 { 792 $options = get_option(self::OPTION_GROUP, []); 793 $field = $args['field']; 794 $current = $options[$field] ?? 'ul'; 795 796 $listTypes = [ 797 'ul' => __('Unordered (bullets)', 'djot-markup'), 798 'ol' => __('Ordered (numbers)', 'djot-markup'), 799 ]; 800 801 printf( 802 '<select id="%1$s" name="%2$s[%1$s]">', 803 esc_attr($field), 804 esc_attr(self::OPTION_GROUP), 805 ); 806 807 foreach ($listTypes as $value => $label) { 808 printf( 809 '<option value="%s" %s>%s</option>', 810 esc_attr($value), 811 selected($current, $value, false), 812 esc_html($label), 813 ); 814 } 815 816 echo '</select>'; 817 818 if (!empty($args['description'])) { 819 printf('<p class="description">%s</p>', esc_html($args['description'])); 820 } 821 } 560 822 } -
djot-markup/tags/1.4.0/src/Converter.php
r3454148 r3454428 11 11 12 12 use Djot\DjotConverter; 13 use Djot\Extension\HeadingPermalinksExtension; 14 use Djot\Extension\SmartQuotesExtension; 15 use Djot\Extension\TableOfContentsExtension; 13 16 use Djot\Profile; 14 17 use Djot\Renderer\SoftBreakMode; … … 38 41 39 42 private bool $markdownMode; 43 44 private bool $tocEnabled; 45 46 private string $tocPosition; 47 48 private int $tocMinLevel; 49 50 private int $tocMaxLevel; 51 52 private string $tocListType; 53 54 private bool $permalinksEnabled; 55 56 private string $smartQuotesLocale; 40 57 41 58 /** … … 51 68 string $commentSoftBreak = 'newline', 52 69 bool $markdownMode = false, 70 bool $tocEnabled = false, 71 string $tocPosition = 'top', 72 int $tocMinLevel = 2, 73 int $tocMaxLevel = 4, 74 string $tocListType = 'ul', 75 bool $permalinksEnabled = false, 76 string $smartQuotesLocale = 'en', 53 77 ) { 54 78 $this->defaultSafeMode = $safeMode; … … 58 82 $this->commentSoftBreak = $commentSoftBreak; 59 83 $this->markdownMode = $markdownMode; 84 $this->tocEnabled = $tocEnabled; 85 $this->tocPosition = $tocPosition; 86 $this->tocMinLevel = $tocMinLevel; 87 $this->tocMaxLevel = $tocMaxLevel; 88 $this->tocListType = $tocListType; 89 $this->permalinksEnabled = $permalinksEnabled; 90 $this->smartQuotesLocale = $smartQuotesLocale; 60 91 $this->converter = new DjotConverter(safeMode: false); 61 92 $this->converter->getRenderer()->setCodeBlockTabWidth(4); … … 80 111 commentSoftBreak: $options['comment_soft_break'] ?? 'newline', 81 112 markdownMode: !empty($options['markdown_mode']), 113 tocEnabled: !empty($options['toc_enabled']), 114 tocPosition: $options['toc_position'] ?? 'top', 115 tocMinLevel: (int)($options['toc_min_level'] ?? 2), 116 tocMaxLevel: (int)($options['toc_max_level'] ?? 4), 117 tocListType: $options['toc_list_type'] ?? 'ul', 118 permalinksEnabled: !empty($options['permalinks_enabled']), 119 smartQuotesLocale: $options['smart_quotes_locale'] ?? 'en', 82 120 ); 83 121 } … … 93 131 { 94 132 $softBreakSetting = $context === 'comment' ? $this->commentSoftBreak : $this->postSoftBreak; 95 $key = $profileName . ($safeMode ? '_safe' : '_unsafe') . '_' . $softBreakSetting . ($this->markdownMode ? '_md' : ''); 133 $tocKey = ($this->tocEnabled && $context === 'article') 134 ? '_toc_' . $this->tocPosition . '_' . $this->tocMinLevel . '_' . $this->tocMaxLevel . '_' . $this->tocListType 135 : ''; 136 $permalinksKey = ($this->permalinksEnabled && $context === 'article') ? '_permalinks' : ''; 137 $smartQuotesKey = $this->smartQuotesLocale !== 'en' ? '_sq_' . $this->smartQuotesLocale : ''; 138 $key = $profileName . ($safeMode ? '_safe' : '_unsafe') . '_' . $softBreakSetting . ($this->markdownMode ? '_md' : '') . $tocKey . $permalinksKey . $smartQuotesKey; 96 139 97 140 if (!isset($this->profileConverters[$key])) { … … 123 166 // Convert tabs to 4 spaces in code blocks for consistent display 124 167 $converter->getRenderer()->setCodeBlockTabWidth(4); 168 169 // Add Table of Contents extension for articles when enabled 170 if ($this->tocEnabled && $context === 'article') { 171 $tocExtension = new TableOfContentsExtension( 172 minLevel: $this->tocMinLevel, 173 maxLevel: $this->tocMaxLevel, 174 listType: $this->tocListType, 175 cssClass: 'wpdjot-toc', 176 position: $this->tocPosition, 177 ); 178 $converter->addExtension($tocExtension); 179 180 // Wrap TOC in collapsible <details>/<summary> 181 $converter->addOutputTransformer(function (string $html): string { 182 $label = __('Table of Contents', 'djot-markup'); 183 184 return (string)preg_replace( 185 '#<nav class="wpdjot-toc">\n(.*?)</nav>\n#s', 186 '<details class="wpdjot-toc">' . "\n" 187 . '<summary>' . esc_html($label) . '</summary>' . "\n" 188 . '$1</details>' . "\n", 189 $html, 190 ); 191 }); 192 } 193 194 // Add heading permalinks for articles when enabled 195 if ($this->permalinksEnabled && $context === 'article') { 196 $converter->addExtension(new HeadingPermalinksExtension( 197 symbol: '#', 198 cssClass: 'wpdjot-permalink', 199 )); 200 } 201 202 // Add smart quotes extension for non-English locales 203 if ($this->smartQuotesLocale !== 'en') { 204 $locale = $this->smartQuotesLocale === 'auto' ? $this->getWpLocale() : $this->smartQuotesLocale; 205 $converter->addExtension(new SmartQuotesExtension(locale: $locale)); 206 } 125 207 126 208 // Allow customization via WordPress filters … … 367 449 return $allowedHtml; 368 450 } 451 452 /** 453 * Get the WordPress locale, falling back to 'en'. 454 */ 455 private function getWpLocale(): string 456 { 457 if (function_exists('get_locale')) { 458 return get_locale() ?: 'en'; 459 } 460 461 return 'en'; 462 } 369 463 } -
djot-markup/tags/1.4.0/src/Plugin.php
r3454148 r3454428 491 491 'document.addEventListener("DOMContentLoaded", function() { hljs.highlightAll(); });', 492 492 ); 493 } 494 495 // Heading permalink copy-to-clipboard 496 if ($this->options['permalinks_enabled']) { 497 $copyJs = 'document.addEventListener("click",function(e){' 498 . 'var a=e.target.closest(".wpdjot-permalink");' 499 . 'if(!a)return;' 500 . 'e.preventDefault();' 501 . 'var hash=a.getAttribute("href");' 502 . 'history.replaceState(null,null,hash);' 503 . 'navigator.clipboard.writeText(location.href);' 504 . '});'; 505 wp_register_script('wpdjot-permalink', false, [], (string)WPDJOT_VERSION, ['in_footer' => true]); 506 wp_enqueue_script('wpdjot-permalink'); 507 wp_add_inline_script('wpdjot-permalink', $copyJs); 493 508 } 494 509 … … 533 548 'highlight_theme' => 'github', 534 549 'shortcode_tag' => 'djot', 550 'toc_enabled' => false, 551 'toc_position' => 'top', 552 'toc_min_level' => 2, 553 'toc_max_level' => 4, 554 'toc_list_type' => 'ul', 555 'permalinks_enabled' => false, 556 'smart_quotes_locale' => 'en', 535 557 ]; 536 558 -
djot-markup/tags/1.4.0/vendor/autoload.php
r3454148 r3454428 20 20 require_once __DIR__ . '/composer/autoload_real.php'; 21 21 22 return ComposerAutoloaderInit 4fce3a77000f5adc49d1807e59eeac64::getLoader();22 return ComposerAutoloaderInit03e39cad62747481332e0a708bd351b8::getLoader(); -
djot-markup/tags/1.4.0/vendor/composer/autoload_classmap.php
r3454148 r3454428 22 22 'Djot\\Extension\\HeadingPermalinksExtension' => $vendorDir . '/php-collective/djot/src/Extension/HeadingPermalinksExtension.php', 23 23 'Djot\\Extension\\MentionsExtension' => $vendorDir . '/php-collective/djot/src/Extension/MentionsExtension.php', 24 'Djot\\Extension\\SmartQuotesExtension' => $vendorDir . '/php-collective/djot/src/Extension/SmartQuotesExtension.php', 24 25 'Djot\\Extension\\TableOfContentsExtension' => $vendorDir . '/php-collective/djot/src/Extension/TableOfContentsExtension.php', 25 26 'Djot\\Filter\\ProfileFilter' => $vendorDir . '/php-collective/djot/src/Filter/ProfileFilter.php', … … 80 81 'Djot\\Profile' => $vendorDir . '/php-collective/djot/src/Profile.php', 81 82 'Djot\\ProfileViolation' => $vendorDir . '/php-collective/djot/src/ProfileViolation.php', 83 'Djot\\Renderer\\HeadingIdTracker' => $vendorDir . '/php-collective/djot/src/Renderer/HeadingIdTracker.php', 82 84 'Djot\\Renderer\\HtmlRenderer' => $vendorDir . '/php-collective/djot/src/Renderer/HtmlRenderer.php', 83 85 'Djot\\Renderer\\MarkdownRenderer' => $vendorDir . '/php-collective/djot/src/Renderer/MarkdownRenderer.php', … … 87 89 'Djot\\Renderer\\Utility\\EventDispatcherTrait' => $vendorDir . '/php-collective/djot/src/Renderer/Utility/EventDispatcherTrait.php', 88 90 'Djot\\SafeMode' => $vendorDir . '/php-collective/djot/src/SafeMode.php', 91 'WpDjot\\Admin\\Settings' => $baseDir . '/src/Admin/Settings.php', 92 'WpDjot\\Blocks\\DjotBlock' => $baseDir . '/src/Blocks/DjotBlock.php', 93 'WpDjot\\CLI\\MigrateCommand' => $baseDir . '/src/CLI/MigrateCommand.php', 94 'WpDjot\\Converter' => $baseDir . '/src/Converter.php', 95 'WpDjot\\Converter\\WpHtmlToDjot' => $baseDir . '/src/Converter/WpHtmlToDjot.php', 96 'WpDjot\\Converter\\WpMarkdownToDjot' => $baseDir . '/src/Converter/WpMarkdownToDjot.php', 97 'WpDjot\\Migration\\Migrator' => $baseDir . '/src/Migration/Migrator.php', 98 'WpDjot\\Plugin' => $baseDir . '/src/Plugin.php', 99 'WpDjot\\Shortcodes\\DjotShortcode' => $baseDir . '/src/Shortcodes/DjotShortcode.php', 89 100 ); -
djot-markup/tags/1.4.0/vendor/composer/autoload_real.php
r3454148 r3454428 3 3 // autoload_real.php @generated by Composer 4 4 5 class ComposerAutoloaderInit 4fce3a77000f5adc49d1807e59eeac645 class ComposerAutoloaderInit03e39cad62747481332e0a708bd351b8 6 6 { 7 7 private static $loader; … … 25 25 require __DIR__ . '/platform_check.php'; 26 26 27 spl_autoload_register(array('ComposerAutoloaderInit 4fce3a77000f5adc49d1807e59eeac64', 'loadClassLoader'), true, true);27 spl_autoload_register(array('ComposerAutoloaderInit03e39cad62747481332e0a708bd351b8', 'loadClassLoader'), true, true); 28 28 self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); 29 spl_autoload_unregister(array('ComposerAutoloaderInit 4fce3a77000f5adc49d1807e59eeac64', 'loadClassLoader'));29 spl_autoload_unregister(array('ComposerAutoloaderInit03e39cad62747481332e0a708bd351b8', 'loadClassLoader')); 30 30 31 31 require __DIR__ . '/autoload_static.php'; 32 call_user_func(\Composer\Autoload\ComposerStaticInit 4fce3a77000f5adc49d1807e59eeac64::getInitializer($loader));32 call_user_func(\Composer\Autoload\ComposerStaticInit03e39cad62747481332e0a708bd351b8::getInitializer($loader)); 33 33 34 34 $loader->register(true); 35 35 36 $filesToLoad = \Composer\Autoload\ComposerStaticInit 4fce3a77000f5adc49d1807e59eeac64::$files;36 $filesToLoad = \Composer\Autoload\ComposerStaticInit03e39cad62747481332e0a708bd351b8::$files; 37 37 $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { 38 38 if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { -
djot-markup/tags/1.4.0/vendor/composer/autoload_static.php
r3454148 r3454428 5 5 namespace Composer\Autoload; 6 6 7 class ComposerStaticInit 4fce3a77000f5adc49d1807e59eeac647 class ComposerStaticInit03e39cad62747481332e0a708bd351b8 8 8 { 9 9 public static $files = array ( … … 49 49 'Djot\\Extension\\HeadingPermalinksExtension' => __DIR__ . '/..' . '/php-collective/djot/src/Extension/HeadingPermalinksExtension.php', 50 50 'Djot\\Extension\\MentionsExtension' => __DIR__ . '/..' . '/php-collective/djot/src/Extension/MentionsExtension.php', 51 'Djot\\Extension\\SmartQuotesExtension' => __DIR__ . '/..' . '/php-collective/djot/src/Extension/SmartQuotesExtension.php', 51 52 'Djot\\Extension\\TableOfContentsExtension' => __DIR__ . '/..' . '/php-collective/djot/src/Extension/TableOfContentsExtension.php', 52 53 'Djot\\Filter\\ProfileFilter' => __DIR__ . '/..' . '/php-collective/djot/src/Filter/ProfileFilter.php', … … 107 108 'Djot\\Profile' => __DIR__ . '/..' . '/php-collective/djot/src/Profile.php', 108 109 'Djot\\ProfileViolation' => __DIR__ . '/..' . '/php-collective/djot/src/ProfileViolation.php', 110 'Djot\\Renderer\\HeadingIdTracker' => __DIR__ . '/..' . '/php-collective/djot/src/Renderer/HeadingIdTracker.php', 109 111 'Djot\\Renderer\\HtmlRenderer' => __DIR__ . '/..' . '/php-collective/djot/src/Renderer/HtmlRenderer.php', 110 112 'Djot\\Renderer\\MarkdownRenderer' => __DIR__ . '/..' . '/php-collective/djot/src/Renderer/MarkdownRenderer.php', … … 114 116 'Djot\\Renderer\\Utility\\EventDispatcherTrait' => __DIR__ . '/..' . '/php-collective/djot/src/Renderer/Utility/EventDispatcherTrait.php', 115 117 'Djot\\SafeMode' => __DIR__ . '/..' . '/php-collective/djot/src/SafeMode.php', 118 'WpDjot\\Admin\\Settings' => __DIR__ . '/../..' . '/src/Admin/Settings.php', 119 'WpDjot\\Blocks\\DjotBlock' => __DIR__ . '/../..' . '/src/Blocks/DjotBlock.php', 120 'WpDjot\\CLI\\MigrateCommand' => __DIR__ . '/../..' . '/src/CLI/MigrateCommand.php', 121 'WpDjot\\Converter' => __DIR__ . '/../..' . '/src/Converter.php', 122 'WpDjot\\Converter\\WpHtmlToDjot' => __DIR__ . '/../..' . '/src/Converter/WpHtmlToDjot.php', 123 'WpDjot\\Converter\\WpMarkdownToDjot' => __DIR__ . '/../..' . '/src/Converter/WpMarkdownToDjot.php', 124 'WpDjot\\Migration\\Migrator' => __DIR__ . '/../..' . '/src/Migration/Migrator.php', 125 'WpDjot\\Plugin' => __DIR__ . '/../..' . '/src/Plugin.php', 126 'WpDjot\\Shortcodes\\DjotShortcode' => __DIR__ . '/../..' . '/src/Shortcodes/DjotShortcode.php', 116 127 ); 117 128 … … 119 130 { 120 131 return \Closure::bind(function () use ($loader) { 121 $loader->prefixLengthsPsr4 = ComposerStaticInit 4fce3a77000f5adc49d1807e59eeac64::$prefixLengthsPsr4;122 $loader->prefixDirsPsr4 = ComposerStaticInit 4fce3a77000f5adc49d1807e59eeac64::$prefixDirsPsr4;123 $loader->classMap = ComposerStaticInit 4fce3a77000f5adc49d1807e59eeac64::$classMap;132 $loader->prefixLengthsPsr4 = ComposerStaticInit03e39cad62747481332e0a708bd351b8::$prefixLengthsPsr4; 133 $loader->prefixDirsPsr4 = ComposerStaticInit03e39cad62747481332e0a708bd351b8::$prefixDirsPsr4; 134 $loader->classMap = ComposerStaticInit03e39cad62747481332e0a708bd351b8::$classMap; 124 135 125 136 }, null, ClassLoader::class); -
djot-markup/tags/1.4.0/vendor/composer/installed.json
r3454148 r3454428 3 3 { 4 4 "name": "php-collective/djot", 5 "version": "0.1.1 2",6 "version_normalized": "0.1.1 2.0",5 "version": "0.1.13", 6 "version_normalized": "0.1.13.0", 7 7 "source": { 8 8 "type": "git", 9 9 "url": "https://github.com/php-collective/djot-php.git", 10 "reference": " 0b8e06827b24e0447acfb6c79f6ff717b90440d1"10 "reference": "c79cba85d26483246e7d8b1bfdf387c0dad3d489" 11 11 }, 12 12 "dist": { 13 13 "type": "zip", 14 "url": "https://api.github.com/repos/php-collective/djot-php/zipball/ 0b8e06827b24e0447acfb6c79f6ff717b90440d1",15 "reference": " 0b8e06827b24e0447acfb6c79f6ff717b90440d1",14 "url": "https://api.github.com/repos/php-collective/djot-php/zipball/c79cba85d26483246e7d8b1bfdf387c0dad3d489", 15 "reference": "c79cba85d26483246e7d8b1bfdf387c0dad3d489", 16 16 "shasum": "" 17 17 }, … … 25 25 "phpunit/phpunit": "^11.0 || ^12.0" 26 26 }, 27 "time": "2026-0 1-20T05:36:44+00:00",27 "time": "2026-02-05T08:44:43+00:00", 28 28 "bin": [ 29 29 "bin/djot" … … 55 55 "support": { 56 56 "issues": "https://github.com/php-collective/djot-php/issues", 57 "source": "https://github.com/php-collective/djot-php/tree/0.1.1 2"57 "source": "https://github.com/php-collective/djot-php/tree/0.1.13" 58 58 }, 59 59 "funding": [ -
djot-markup/tags/1.4.0/vendor/composer/installed.php
r3454148 r3454428 2 2 'root' => array( 3 3 'name' => 'php-collective/wp-djot', 4 'pretty_version' => '1. 0.0+no-version-set',5 'version' => '1. 0.0.0',6 'reference' => null,4 'pretty_version' => '1.4.0', 5 'version' => '1.4.0.0', 6 'reference' => '8557458d2d6cb8cc4b406577b26b2afc51868377', 7 7 'type' => 'wordpress-plugin', 8 8 'install_path' => __DIR__ . '/../../', … … 12 12 'versions' => array( 13 13 'php-collective/djot' => array( 14 'pretty_version' => '0.1.1 2',15 'version' => '0.1.1 2.0',16 'reference' => ' 0b8e06827b24e0447acfb6c79f6ff717b90440d1',14 'pretty_version' => '0.1.13', 15 'version' => '0.1.13.0', 16 'reference' => 'c79cba85d26483246e7d8b1bfdf387c0dad3d489', 17 17 'type' => 'library', 18 18 'install_path' => __DIR__ . '/../php-collective/djot', … … 21 21 ), 22 22 'php-collective/wp-djot' => array( 23 'pretty_version' => '1. 0.0+no-version-set',24 'version' => '1. 0.0.0',25 'reference' => null,23 'pretty_version' => '1.4.0', 24 'version' => '1.4.0.0', 25 'reference' => '8557458d2d6cb8cc4b406577b26b2afc51868377', 26 26 'type' => 'wordpress-plugin', 27 27 'install_path' => __DIR__ . '/../../', -
djot-markup/tags/1.4.0/vendor/php-collective/djot/src/DjotConverter.php
r3454148 r3454428 10 10 use Djot\Node\Document; 11 11 use Djot\Parser\BlockParser; 12 use Djot\Renderer\HeadingIdTracker; 12 13 use Djot\Renderer\HtmlRenderer; 13 14 use Djot\Renderer\SoftBreakMode; … … 280 281 281 282 /** 283 * Get the heading ID tracker 284 */ 285 public function getHeadingIdTracker(): HeadingIdTracker 286 { 287 return $this->renderer->getHeadingIdTracker(); 288 } 289 290 /** 282 291 * Get the block parser for direct access 283 292 */ -
djot-markup/tags/1.4.0/vendor/php-collective/djot/src/Extension/HeadingPermalinksExtension.php
r3454148 r3454428 8 8 use Djot\Event\RenderEvent; 9 9 use Djot\Node\Block\Heading; 10 use Djot\Node\Inline\HardBreak;11 10 use Djot\Node\Inline\Link; 12 use Djot\Node\Inline\SoftBreak;13 11 use Djot\Node\Inline\Span; 14 12 use Djot\Node\Inline\Text; 15 use Djot\Node\Node;16 13 17 14 /** … … 33 30 * ariaLabel: 'Link to this section', 34 31 * )); 32 * 33 * // Show on hover only, with copy-to-clipboard: 34 * $converter->addExtension(new HeadingPermalinksExtension( 35 * showOnHover: true, // adds 'permalink-hover' class to wrapper 36 * copyToClipboard: true, // adds 'data-permalink-copy' attribute to link 37 * )); 38 * ``` 39 * 40 * When `showOnHover` is enabled, add CSS like: 41 * ```css 42 * .permalink-hover { opacity: 0; transition: opacity .2s; } 43 * h1:hover > .permalink-hover, h2:hover > .permalink-hover, 44 * h3:hover > .permalink-hover, h4:hover > .permalink-hover, 45 * h5:hover > .permalink-hover, h6:hover > .permalink-hover, 46 * .permalink-hover:focus-within { opacity: 1; } 47 * ``` 48 * 49 * When `copyToClipboard` is enabled, add JavaScript like: 50 * ```javascript 51 * document.addEventListener('click', function(e) { 52 * var a = e.target.closest('[data-permalink-copy]'); 53 * if (!a) return; 54 * e.preventDefault(); 55 * navigator.clipboard.writeText(a.href); 56 * history.replaceState(null, null, a.getAttribute('href')); 57 * }); 35 58 * ``` 36 59 * … … 50 73 * @param string $ariaLabel Accessibility label for the link 51 74 * @param array<int> $levels Which heading levels to add permalinks to (1-6) 75 * @param bool $showOnHover Only show permalink on heading hover (injects CSS) 76 * @param bool $copyToClipboard Copy URL to clipboard on click instead of navigating (injects JS) 52 77 */ 53 78 public function __construct( … … 57 82 protected string $ariaLabel = 'Permalink', 58 83 protected array $levels = [1, 2, 3, 4, 5, 6], 84 protected bool $showOnHover = false, 85 protected bool $copyToClipboard = false, 59 86 ) { 60 87 } … … 62 89 public function register(DjotConverter $converter): void 63 90 { 64 $converter->on('render.heading', function (RenderEvent $event): void { 91 $tracker = $converter->getHeadingIdTracker(); 92 93 $converter->on('render.heading', function (RenderEvent $event) use ($tracker): void { 65 94 $node = $event->getNode(); 66 95 if (!$node instanceof Heading) { … … 72 101 } 73 102 74 // Get the ID (either explicit or will be generated by renderer) 75 /** @var string|null $id */ 76 $id = $node->getAttribute('id'); 77 if ($id === null || $id === '') { 78 // Generate ID from heading text (matching renderer logic) 79 $id = $this->generateIdFromNode($node); 80 // Set it explicitly so the renderer uses this ID, not one generated 81 // from the modified heading content (which would include the permalink symbol) 82 if ($id !== '') { 83 $node->setAttribute('id', $id); 84 } 85 } 103 // Get the deduplicated ID from the shared tracker 104 $id = $tracker->getIdForHeading($node); 86 105 87 106 if ($id === '') { 88 107 return; 108 } 109 110 // Set it explicitly so the renderer uses this ID, not one generated 111 // from the modified heading content (which would include the permalink symbol) 112 if (!$node->hasAttribute('id')) { 113 $node->setAttribute('id', $id); 89 114 } 90 115 … … 95 120 $link->appendChild(new Text($this->symbol)); 96 121 122 if ($this->copyToClipboard) { 123 $link->setAttribute('data-permalink-copy', ''); 124 } 125 97 126 // Wrap in span for styling flexibility 98 127 $span = new Span(); 99 128 $span->addClass('permalink-wrapper'); 129 if ($this->showOnHover) { 130 $span->addClass('permalink-hover'); 131 } 100 132 $span->appendChild($link); 101 133 102 134 // Add to heading 103 135 if ($this->position === 'before') { 104 // Prepend space + span, then the space105 136 $space = new Text(' '); 106 137 $node->prependChild($space); 107 138 $node->prependChild($span); 108 139 } else { 109 // Add space before110 140 $node->appendChild(new Text(' ')); 111 141 $node->appendChild($span); … … 113 143 }); 114 144 } 115 116 /**117 * Generate a slug from heading content (matches HtmlRenderer behavior)118 */119 protected function generateIdFromNode(Heading $node): string120 {121 $text = $this->getPlainText($node);122 if ($text === '') {123 return '';124 }125 126 // Match HtmlRenderer::getSectionId() behavior:127 // 1. Strip # characters entirely128 // 2. Trim whitespace129 // 3. Replace whitespace sequences with single dashes130 $id = str_replace('#', '', $text);131 $id = trim($id);132 $id = preg_replace('/[\s]+/', '-', $id) ?? $id;133 134 return $id;135 }136 137 /**138 * Extract plain text from a node and its children (recursive)139 */140 protected function getPlainText(Node $node): string141 {142 $text = '';143 foreach ($node->getChildren() as $child) {144 if ($child instanceof Text) {145 $text .= $child->getContent();146 } elseif ($child instanceof SoftBreak || $child instanceof HardBreak) {147 $text .= ' ';148 } elseif ($child instanceof Node) {149 $text .= $this->getPlainText($child);150 }151 }152 153 return $text;154 }155 145 } -
djot-markup/tags/1.4.0/vendor/php-collective/djot/src/Extension/TableOfContentsExtension.php
r3454148 r3454428 7 7 use Djot\DjotConverter; 8 8 use Djot\Node\Block\Heading; 9 use Djot\Node\Inline\Text;10 use Djot\Node\Node;11 9 12 10 /** … … 75 73 public function register(DjotConverter $converter): void 76 74 { 75 $tracker = $converter->getHeadingIdTracker(); 76 77 77 // Hook into heading rendering to collect TOC entries 78 $converter->on('render.heading', function ($event) : void {78 $converter->on('render.heading', function ($event) use ($tracker): void { 79 79 $node = $event->getNode(); 80 80 if (!$node instanceof Heading) { … … 88 88 } 89 89 90 $text = $t his->getPlainText($node);91 $id = $t his->generateId($node, $text);90 $text = $tracker->getPlainText($node); 91 $id = $tracker->getIdForHeading($node); 92 92 93 93 $this->toc[] = [ … … 158 158 159 159 /** 160 * Extract plain text from a node161 */162 protected function getPlainText(Node $node): string163 {164 $text = '';165 foreach ($node->getChildren() as $child) {166 if ($child instanceof Text) {167 $text .= $child->getContent();168 } elseif (method_exists($child, 'getChildren')) {169 $text .= $this->getPlainText($child);170 }171 }172 173 return $text;174 }175 176 /**177 * Generate ID for a heading (matches HtmlRenderer::getSectionId() behavior)178 */179 protected function generateId(Heading $heading, string $text): string180 {181 // Check for explicit ID attribute182 $id = $heading->getAttribute('id');183 if (is_string($id) && $id !== '') {184 return $id;185 }186 187 // Generate from text - match HtmlRenderer::getSectionId() behavior:188 // 1. Strip # characters entirely189 // 2. Trim whitespace190 // 3. Replace whitespace sequences with single dashes191 if ($text === '') {192 return '';193 }194 195 $id = str_replace('#', '', $text);196 $id = trim($id);197 $id = preg_replace('/[\s]+/', '-', $id) ?? $id;198 199 return $id;200 }201 202 /**203 160 * Render TOC as nested HTML list 204 161 * -
djot-markup/tags/1.4.0/vendor/php-collective/djot/src/Parser/InlineParser.php
r3454148 r3454428 65 65 protected ?array $cachedAbbreviations = null; 66 66 67 /** 68 * Smart quote characters (configurable via SmartQuotesExtension for locale support) 69 */ 70 protected string $openDoubleQuote = "\u{201C}"; 71 72 protected string $closeDoubleQuote = "\u{201D}"; 73 74 protected string $openSingleQuote = "\u{2018}"; 75 76 protected string $closeSingleQuote = "\u{2019}"; 77 78 /** 79 * Apostrophe character (always U+2019 RIGHT SINGLE QUOTATION MARK) 80 * 81 * Not configurable via extension — apostrophes are language-independent. 82 */ 83 protected string $apostrophe = "\u{2019}"; 84 67 85 public function __construct(protected BlockParser $blockParser) 68 86 { … … 117 135 { 118 136 return $this->customPatterns; 137 } 138 139 /** 140 * Set locale-specific smart quote characters 141 * 142 * Apostrophes (mid-word and before digits) always remain U+2019 143 * regardless of this setting. 144 */ 145 public function setQuoteCharacters( 146 string $openDoubleQuote, 147 string $closeDoubleQuote, 148 string $openSingleQuote, 149 string $closeSingleQuote, 150 ): void { 151 $this->openDoubleQuote = $openDoubleQuote; 152 $this->closeDoubleQuote = $closeDoubleQuote; 153 $this->openSingleQuote = $openSingleQuote; 154 $this->closeSingleQuote = $closeSingleQuote; 155 } 156 157 /** 158 * Get the current smart quote characters 159 * 160 * @return array{openDouble: string, closeDouble: string, openSingle: string, closeSingle: string} 161 */ 162 public function getQuoteCharacters(): array 163 { 164 return [ 165 'openDouble' => $this->openDoubleQuote, 166 'closeDouble' => $this->closeDoubleQuote, 167 'openSingle' => $this->openSingleQuote, 168 'closeSingle' => $this->closeSingleQuote, 169 ]; 119 170 } 120 171 … … 1055 1106 // Must be followed by closing } 1056 1107 if ($quotePos < $length && $text[$quotePos] === '}') { 1057 // Generate curlyquotes based on count1058 $openQuote = $marker === "'" ? "\u{2018}" : "\u{201C}";1059 $closeQuote = $marker === "'" ? "\u{2019}" : "\u{201D}";1108 // Generate quotes based on count 1109 $openQuote = $marker === "'" ? $this->openSingleQuote : $this->openDoubleQuote; 1110 $closeQuote = $marker === "'" ? $this->closeSingleQuote : $this->closeDoubleQuote; 1060 1111 1061 1112 // For pairs like {''}, output left + right 1062 // For single {'}, output just right (used for apostrophe)1113 // For single {'}, output apostrophe (always U+2019), {"} output close double 1063 1114 if ($quoteCount === 1) { 1064 $result = $ closeQuote;1115 $result = $marker === "'" ? $this->apostrophe : $closeQuote; 1065 1116 } elseif ($quoteCount === 2) { 1066 1117 $result = $openQuote . $closeQuote; … … 1122 1173 // Quote immediately after = is always an opener (attribute value start) 1123 1174 if ($prevChar === '=') { 1124 return $quote === '"' ? "\u{201C}" : "\u{2018}";1175 return $quote === '"' ? $this->openDoubleQuote : $this->openSingleQuote; 1125 1176 } 1126 1177 … … 1147 1198 // Single quote before digit is always apostrophe (e.g., '70s) 1148 1199 if ($quote === "'" && ctype_digit($nextChar)) { 1149 return "\u{2019}"; // closing/apostrophe1200 return $this->apostrophe; 1150 1201 } 1151 1202 1152 1203 // A quote after ] or ) cannot be an opener 1153 1204 if ($prevChar === ']' || $prevChar === ')') { 1154 return $quote === '"' ? "\u{201D}" : "\u{2019}";1205 return $quote === '"' ? $this->closeDoubleQuote : $this->closeSingleQuote; 1155 1206 } 1156 1207 1157 1208 if ($quote === '"') { 1158 1209 // Opening if preceded by space or start, closing otherwise 1159 return $prevIsSpace && !$nextIsSpace ? "\u{201C}" : "\u{201D}";1210 return $prevIsSpace && !$nextIsSpace ? $this->openDoubleQuote : $this->closeDoubleQuote; 1160 1211 } 1161 1212 … … 1166 1217 $matchingCloser = $this->findMatchingSingleQuoteCloser($text, $pos); 1167 1218 if ($matchingCloser !== null) { 1168 return "\u{2018}"; // opening quote1219 return $this->openSingleQuote; 1169 1220 } 1170 1221 1171 1222 // No matching closer found, treat as apostrophe 1172 return "\u{2019}"; 1173 } 1174 1175 // Closing/apostrophe 1176 return "\u{2019}"; 1223 return $this->apostrophe; 1224 } 1225 1226 // Check if this is mid-word (next char is a word character) — apostrophe 1227 if (preg_match('/\w/u', $nextChar)) { 1228 return $this->apostrophe; 1229 } 1230 1231 // Closing single quote 1232 return $this->closeSingleQuote; 1177 1233 } 1178 1234 … … 1359 1415 } 1360 1416 1361 // Check for multi-byte UTF-8 curly quotes (3 bytes each)1417 // Check for multi-byte configured quote characters 1362 1418 // These act as word boundaries for attribute attachment 1363 if (ord($char) >= 0x98 && ord($char) <= 0x9D && $wordStart >= 3) { 1364 $threeBytes = substr($textBuffer, $wordStart - 3, 3); 1365 // Check for curly quotes: " " ' ' (U+201C, U+201D, U+2018, U+2019) 1366 if ( 1367 $threeBytes === "\u{201C}" || $threeBytes === "\u{201D}" || 1368 $threeBytes === "\u{2018}" || $threeBytes === "\u{2019}" 1369 ) { 1370 break; 1419 foreach ($this->getConfiguredQuoteStrings() as $quoteStr) { 1420 $quoteLen = strlen($quoteStr); 1421 if ($wordStart >= $quoteLen && substr($textBuffer, $wordStart - $quoteLen, $quoteLen) === $quoteStr) { 1422 break 2; 1371 1423 } 1372 1424 } … … 1406 1458 'pos' => $attrEnd + 1, 1407 1459 ]; 1460 } 1461 1462 /** 1463 * Get all unique configured quote strings for word boundary detection 1464 * 1465 * @return array<string> 1466 */ 1467 protected function getConfiguredQuoteStrings(): array 1468 { 1469 return array_unique([ 1470 $this->openDoubleQuote, 1471 $this->closeDoubleQuote, 1472 $this->openSingleQuote, 1473 $this->closeSingleQuote, 1474 $this->apostrophe, 1475 ]); 1408 1476 } 1409 1477 -
djot-markup/tags/1.4.0/vendor/php-collective/djot/src/Renderer/HtmlRenderer.php
r3454148 r3454428 71 71 protected array $footnoteRefCounts = []; 72 72 73 /** 74 * Tracks used IDs for deduplication 75 * 76 * @var array<string, int> 77 */ 78 protected array $usedIds = []; 79 80 /** 81 * Counter for auto-generated section IDs (when heading has no text) 82 */ 83 protected int $sectionCounter = 0; 73 protected HeadingIdTracker $headingIdTracker; 84 74 85 75 /** … … 116 106 public function __construct(protected bool $xhtml = false) 117 107 { 108 $this->headingIdTracker = new HeadingIdTracker(); 118 109 $this->initNodeRenderers(); 110 } 111 112 /** 113 * Get the heading ID tracker 114 */ 115 public function getHeadingIdTracker(): HeadingIdTracker 116 { 117 return $this->headingIdTracker; 119 118 } 120 119 … … 247 246 // Reset state for each render 248 247 $this->footnoteRefCounts = []; 249 $this->usedIds = []; 250 $this->sectionCounter = 0; 248 $this->headingIdTracker->reset(); 251 249 $this->footnoteNumbers = []; 252 250 $this->footnoteCounter = 0; … … 337 335 protected function getSectionId(Heading $node): string 338 336 { 339 // If heading has explicit id attribute, use it 337 return $this->headingIdTracker->getIdForHeading($node); 338 } 339 340 /** 341 * Track ID usage from non-heading elements (like paragraphs with explicit IDs) 342 */ 343 protected function trackIdFromNode(Node $node): void 344 { 340 345 if ($node->hasAttribute('id')) { 341 346 $idAttr = $node->getAttribute('id'); 342 347 $id = is_string($idAttr) ? $idAttr : ''; 343 // Track explicit IDs so auto-generated IDs don't conflict 344 if (!isset($this->usedIds[$id])) { 345 $this->usedIds[$id] = 0; 346 } 347 348 return $id; 349 } 350 351 // Generate from heading text 352 $headingText = $this->getPlainText($node); 353 354 if ($headingText === '') { 355 // Generate fallback ID 356 $this->sectionCounter++; 357 358 return 's-' . $this->sectionCounter; 359 } 360 361 // Convert to valid ID: 362 // 1. Strip # characters entirely 363 // 2. Trim whitespace 364 // 3. Replace whitespace sequences with single dashes 365 $baseId = str_replace('#', '', $headingText); 366 $baseId = trim($baseId); 367 $baseId = preg_replace('/[\s]+/', '-', $baseId) ?? $baseId; 368 369 // Track and deduplicate 370 if (!isset($this->usedIds[$baseId])) { 371 $this->usedIds[$baseId] = 0; 372 373 return $baseId; 374 } 375 376 // Already used, add suffix (first conflict is -1, second is -2, etc.) 377 $this->usedIds[$baseId]++; 378 379 return $baseId . '-' . $this->usedIds[$baseId]; 380 } 381 382 /** 383 * Track ID usage from non-heading elements (like paragraphs with explicit IDs) 384 */ 385 protected function trackIdFromNode(Node $node): void 386 { 387 if ($node->hasAttribute('id')) { 388 $idAttr = $node->getAttribute('id'); 389 $id = is_string($idAttr) ? $idAttr : ''; 390 if ($id !== '' && !isset($this->usedIds[$id])) { 391 $this->usedIds[$id] = 0; 392 } 348 $this->headingIdTracker->trackId($id); 393 349 } 394 350 } … … 516 472 protected function getPlainText(Node $node): string 517 473 { 518 $text = ''; 519 foreach ($node->getChildren() as $child) { 520 if ($child instanceof Text) { 521 $text .= $child->getContent(); 522 } elseif ($child instanceof SoftBreak || $child instanceof HardBreak) { 523 $text .= ' '; 524 } elseif ($child instanceof Node) { 525 $text .= $this->getPlainText($child); 526 } 527 } 528 529 return $text; 474 return $this->headingIdTracker->getPlainText($node); 530 475 } 531 476 -
djot-markup/tags/1.4.0/wp-djot.php
r3454148 r3454428 2 2 /** 3 3 * Plugin Name: Djot Markup 4 * Plugin URI: https:// github.com/php-collective/wp-djot4 * Plugin URI: https://wordpress.org/plugins/djot-markup/ 5 5 * Description: <a href="https://djot.net/" target="_blank">Djot</a> markup language support for WordPress – a modern, cleaner alternative to Markdown with syntax highlighting. Convert Djot syntax to HTML in posts, pages, and comments. 6 * Version: 1. 3.16 * Version: 1.4.0 7 7 * Requires at least: 6.0 8 8 * Requires PHP: 8.2 … … 25 25 26 26 // Plugin constants 27 define('WPDJOT_VERSION', '1. 3.1');27 define('WPDJOT_VERSION', '1.4.0'); 28 28 define('WPDJOT_PLUGIN_DIR', plugin_dir_path(__FILE__)); 29 29 define('WPDJOT_PLUGIN_URL', plugin_dir_url(__FILE__)); -
djot-markup/trunk/assets/blocks/djot/block.json
r3454148 r3454428 3 3 "apiVersion": 3, 4 4 "name": "wpdjot/djot", 5 "version": "1. 3.1",5 "version": "1.4.0", 6 6 "title": "Djot", 7 7 "category": "text", -
djot-markup/trunk/assets/blocks/djot/index.asset.php
r3454148 r3454428 10 10 'wp-api-fetch', 11 11 ], 12 'version' => '1. 3.1',12 'version' => '1.4.0', 13 13 ]; -
djot-markup/trunk/assets/css/djot.css
r3454148 r3454428 279 279 text-align: center; 280 280 font-style: italic; 281 } 282 283 /* Table of Contents */ 284 .wpdjot-toc { 285 background: #f8f9fa; 286 border: 1px solid #e9ecef; 287 border-radius: 6px; 288 padding: 0; 289 margin-bottom: 1.5em; 290 } 291 292 .wpdjot-toc summary { 293 padding: 0.75em 1.5em; 294 font-weight: 600; 295 cursor: pointer; 296 list-style: none; 297 display: flex; 298 align-items: center; 299 gap: 0.5em; 300 } 301 302 .wpdjot-toc summary::before { 303 content: "▶"; 304 font-size: 0.7em; 305 transition: transform 0.2s ease; 306 } 307 308 .wpdjot-toc[open] summary::before { 309 transform: rotate(90deg); 310 } 311 312 .wpdjot-toc summary::-webkit-details-marker { 313 display: none; 314 } 315 316 .wpdjot-toc > ul, 317 .wpdjot-toc > ol { 318 margin: 0; 319 padding: 0 1.5em 1em; 320 } 321 322 .wpdjot-toc ul, 323 .wpdjot-toc ol { 324 margin: 0; 325 padding-left: 1.5em; 326 } 327 328 .wpdjot-toc li { 329 margin: 0.25em 0; 330 line-height: 1.4; 331 } 332 333 .wpdjot-toc a { 334 color: inherit; 335 text-decoration: none; 336 } 337 338 .wpdjot-toc a:hover { 339 text-decoration: underline; 340 color: #0066cc; 341 } 342 343 /* Heading Permalinks */ 344 .djot-content .permalink-wrapper { 345 opacity: 0; 346 transition: opacity 0.2s ease; 347 margin-left: 0.3em; 348 } 349 350 .djot-content section:hover > h1 .permalink-wrapper, 351 .djot-content section:hover > h2 .permalink-wrapper, 352 .djot-content section:hover > h3 .permalink-wrapper, 353 .djot-content section:hover > h4 .permalink-wrapper, 354 .djot-content section:hover > h5 .permalink-wrapper, 355 .djot-content section:hover > h6 .permalink-wrapper { 356 opacity: 1; 357 } 358 359 .djot-content .wpdjot-permalink { 360 color: #57606a; 361 text-decoration: none; 362 font-weight: 400; 363 cursor: pointer; 364 } 365 366 .djot-content .wpdjot-permalink:hover { 367 color: #0969da; 368 text-decoration: none; 281 369 } 282 370 … … 690 778 color: #8b949e; 691 779 } 692 } 780 781 .wpdjot-toc { 782 background: #161b22; 783 border-color: #30363d; 784 } 785 786 .wpdjot-toc a:hover { 787 color: #58a6ff; 788 } 789 790 .djot-content .wpdjot-permalink { 791 color: #8b949e; 792 } 793 794 .djot-content .wpdjot-permalink:hover { 795 color: #58a6ff; 796 } 797 } -
djot-markup/trunk/composer.json
r3454148 r3454428 12 12 "require": { 13 13 "php": ">=8.2", 14 "php-collective/djot": "^0.1.1 1"14 "php-collective/djot": "^0.1.13" 15 15 }, 16 16 "require-dev": { -
djot-markup/trunk/readme.txt
r3454148 r3454428 5 5 Tested up to: 6.9 6 6 Requires PHP: 8.2 7 Stable tag: 1. 3.17 Stable tag: 1.4.0 8 8 License: MIT 9 9 License URI: https://opensource.org/licenses/MIT … … 20 20 * **Shortcode Support**: Use `[djot]...[/djot]` in your content 21 21 * **Content Filtering**: Automatically process `{djot}...{/djot}` blocks 22 * **Table of Contents**: Automatic TOC generation from headings with configurable levels and position 22 23 * **Safe Mode**: XSS protection for untrusted content 23 24 * **Syntax Highlighting**: Built-in highlight.js integration with 12+ themes … … 90 91 == Changelog == 91 92 93 = 1.4.0 = 94 * Added automatic Table of Contents generation from headings 95 * Configurable TOC position (top/bottom), heading levels, and list type 96 * Light and dark mode styling for TOC 97 * Added heading permalinks with show-on-hover and copy-to-clipboard 98 * Added locale-aware smart quotes (20 locales + Auto from site language) 99 * Bumped php-collective/djot to ^0.1.13 100 92 101 = 1.3.1 = 93 102 * Fixed text domain to match plugin slug (djot-markup) … … 128 137 == Upgrade Notice == 129 138 139 = 1.4.0 = 140 Adds Table of Contents, heading permalinks, and locale-aware smart quotes. 141 130 142 = 1.3.1 = 131 143 Text domain, escaping, and Plugin Check fixes for WordPress.org plugin review compliance. -
djot-markup/trunk/src/Admin/Settings.php
r3454148 r3454428 9 9 exit; 10 10 } 11 12 use Djot\Extension\SmartQuotesExtension; 11 13 12 14 /** … … 206 208 ); 207 209 210 add_settings_field( 211 'smart_quotes_locale', 212 __('Smart Quotes', 'djot-markup'), 213 [$this, 'renderSmartQuotesLocaleSelect'], 214 self::PAGE_SLUG, 215 'wpdjot_rendering', 216 ['field' => 'smart_quotes_locale', 'description' => __('Choose which typographic quote characters to use. Default English uses curly quotes.', 'djot-markup')], 217 ); 218 208 219 // Code Highlighting Section 209 220 add_settings_section( … … 247 258 'wpdjot_advanced', 248 259 ['field' => 'shortcode_tag', 'description' => __('The shortcode tag to use (default: djot).', 'djot-markup')], 260 ); 261 262 // Table of Contents Section 263 add_settings_section( 264 'wpdjot_toc', 265 __('Table of Contents', 'djot-markup'), 266 [$this, 'renderTocSectionDescription'], 267 self::PAGE_SLUG, 268 ); 269 270 add_settings_field( 271 'toc_enabled', 272 __('Enable TOC', 'djot-markup'), 273 [$this, 'renderCheckboxField'], 274 self::PAGE_SLUG, 275 'wpdjot_toc', 276 ['field' => 'toc_enabled', 'description' => __('Automatically generate a table of contents from headings in posts and pages.', 'djot-markup')], 277 ); 278 279 add_settings_field( 280 'toc_position', 281 __('TOC Position', 'djot-markup'), 282 [$this, 'renderTocPositionSelect'], 283 self::PAGE_SLUG, 284 'wpdjot_toc', 285 ['field' => 'toc_position', 'description' => __('Where to insert the table of contents.', 'djot-markup')], 286 ); 287 288 add_settings_field( 289 'toc_min_level', 290 __('Minimum Heading Level', 'djot-markup'), 291 [$this, 'renderHeadingLevelSelect'], 292 self::PAGE_SLUG, 293 'wpdjot_toc', 294 ['field' => 'toc_min_level', 'description' => __('Include headings starting from this level (default: 2 to skip page title).', 'djot-markup')], 295 ); 296 297 add_settings_field( 298 'toc_max_level', 299 __('Maximum Heading Level', 'djot-markup'), 300 [$this, 'renderHeadingLevelSelect'], 301 self::PAGE_SLUG, 302 'wpdjot_toc', 303 ['field' => 'toc_max_level', 'description' => __('Include headings up to this level.', 'djot-markup')], 304 ); 305 306 add_settings_field( 307 'toc_list_type', 308 __('TOC List Type', 'djot-markup'), 309 [$this, 'renderTocListTypeSelect'], 310 self::PAGE_SLUG, 311 'wpdjot_toc', 312 ['field' => 'toc_list_type', 'description' => __('Use ordered (numbered) or unordered (bulleted) list.', 'djot-markup')], 313 ); 314 315 add_settings_field( 316 'permalinks_enabled', 317 __('Heading Permalinks', 'djot-markup'), 318 [$this, 'renderCheckboxField'], 319 self::PAGE_SLUG, 320 'wpdjot_toc', 321 ['field' => 'permalinks_enabled', 'description' => __('Add clickable # symbols to headings. Shown on hover, clicking copies the heading URL to clipboard.', 'djot-markup')], 249 322 ); 250 323 } … … 287 360 ? $input['comment_soft_break'] 288 361 : 'newline', 362 'smart_quotes_locale' => in_array($input['smart_quotes_locale'] ?? '', array_merge(['auto'], SmartQuotesExtension::getSupportedLocales()), true) 363 ? $input['smart_quotes_locale'] 364 : 'en', 365 'toc_enabled' => !empty($input['toc_enabled']), 366 'toc_position' => in_array($input['toc_position'] ?? '', ['top', 'bottom'], true) 367 ? $input['toc_position'] 368 : 'top', 369 'toc_min_level' => in_array((int)($input['toc_min_level'] ?? 2), [1, 2, 3, 4, 5, 6], true) 370 ? (int)$input['toc_min_level'] 371 : 2, 372 'toc_max_level' => in_array((int)($input['toc_max_level'] ?? 4), [1, 2, 3, 4, 5, 6], true) 373 ? (int)$input['toc_max_level'] 374 : 4, 375 'toc_list_type' => in_array($input['toc_list_type'] ?? '', ['ul', 'ol'], true) 376 ? $input['toc_list_type'] 377 : 'ul', 378 'permalinks_enabled' => !empty($input['permalinks_enabled']), 289 379 ]; 290 380 } … … 558 648 echo '</ul>'; 559 649 } 650 651 /** 652 * Render smart quotes locale select dropdown. 653 * 654 * @param array<string, mixed> $args 655 */ 656 public function renderSmartQuotesLocaleSelect(array $args): void 657 { 658 $options = get_option(self::OPTION_GROUP, []); 659 $field = $args['field']; 660 $current = $options[$field] ?? 'en'; 661 662 $localeLabels = [ 663 'en' => "Default (English \u{201C}\u{2026}\u{201D} \u{2018}\u{2026}\u{2019})", 664 'auto' => 'Auto (from site language)', 665 'cs' => "Czech \u{201E}\u{2026}\u{201C} \u{201A}\u{2026}\u{2018}", 666 'da' => "Danish \u{201E}\u{2026}\u{201C} \u{201A}\u{2026}\u{2018}", 667 'nl' => "Dutch \u{201C}\u{2026}\u{201D} \u{2018}\u{2026}\u{2019}", 668 'fi' => "Finnish \u{201D}\u{2026}\u{201D} \u{2019}\u{2026}\u{2019}", 669 'fr' => "French \u{00AB}\u{2026}\u{00BB} \u{2039}\u{2026}\u{203A}", 670 'de' => "German \u{201E}\u{2026}\u{201C} \u{201A}\u{2026}\u{2018}", 671 'de-CH' => "Swiss German \u{00AB}\u{2026}\u{00BB} \u{2039}\u{2026}\u{203A}", 672 'hu' => "Hungarian \u{201E}\u{2026}\u{201D} \u{201A}\u{2026}\u{2019}", 673 'it' => "Italian \u{00AB}\u{2026}\u{00BB} \u{201C}\u{2026}\u{201D}", 674 'ja' => "Japanese \u{300C}\u{2026}\u{300D} \u{300E}\u{2026}\u{300F}", 675 'nb' => "Norwegian Bokm\u{00E5}l \u{00AB}\u{2026}\u{00BB} \u{2018}\u{2026}\u{2019}", 676 'nn' => "Norwegian Nynorsk \u{00AB}\u{2026}\u{00BB} \u{2018}\u{2026}\u{2019}", 677 'pl' => "Polish \u{201E}\u{2026}\u{201D} \u{201A}\u{2026}\u{2019}", 678 'pt' => "Portuguese \u{00AB}\u{2026}\u{00BB} \u{201C}\u{2026}\u{201D}", 679 'ru' => "Russian \u{00AB}\u{2026}\u{00BB} \u{201E}\u{2026}\u{201C}", 680 'es' => "Spanish \u{00AB}\u{2026}\u{00BB} \u{201C}\u{2026}\u{201D}", 681 'sv' => "Swedish \u{201D}\u{2026}\u{201D} \u{2019}\u{2026}\u{2019}", 682 'uk' => "Ukrainian \u{00AB}\u{2026}\u{00BB} \u{201E}\u{2026}\u{201C}", 683 'zh' => "Chinese \u{300C}\u{2026}\u{300D} \u{300E}\u{2026}\u{300F}", 684 ]; 685 686 printf( 687 '<select id="%1$s" name="%2$s[%1$s]">', 688 esc_attr($field), 689 esc_attr(self::OPTION_GROUP), 690 ); 691 692 foreach ($localeLabels as $value => $label) { 693 printf( 694 '<option value="%s" %s>%s</option>', 695 esc_attr($value), 696 selected($current, $value, false), 697 esc_html($label), 698 ); 699 } 700 701 echo '</select>'; 702 703 if (!empty($args['description'])) { 704 printf('<p class="description">%s</p>', esc_html($args['description'])); 705 } 706 } 707 708 public function renderTocSectionDescription(): void 709 { 710 echo '<p>' . esc_html__('Configure automatic table of contents generation for posts and pages.', 'djot-markup') . '</p>'; 711 } 712 713 /** 714 * Render TOC position select dropdown. 715 * 716 * @param array<string, mixed> $args 717 */ 718 public function renderTocPositionSelect(array $args): void 719 { 720 $options = get_option(self::OPTION_GROUP, []); 721 $field = $args['field']; 722 $current = $options[$field] ?? 'top'; 723 724 $positions = [ 725 'top' => __('Top of content', 'djot-markup'), 726 'bottom' => __('Bottom of content', 'djot-markup'), 727 ]; 728 729 printf( 730 '<select id="%1$s" name="%2$s[%1$s]">', 731 esc_attr($field), 732 esc_attr(self::OPTION_GROUP), 733 ); 734 735 foreach ($positions as $value => $label) { 736 printf( 737 '<option value="%s" %s>%s</option>', 738 esc_attr($value), 739 selected($current, $value, false), 740 esc_html($label), 741 ); 742 } 743 744 echo '</select>'; 745 746 if (!empty($args['description'])) { 747 printf('<p class="description">%s</p>', esc_html($args['description'])); 748 } 749 } 750 751 /** 752 * Render heading level select dropdown. 753 * 754 * @param array<string, mixed> $args 755 */ 756 public function renderHeadingLevelSelect(array $args): void 757 { 758 $options = get_option(self::OPTION_GROUP, []); 759 $field = $args['field']; 760 $default = $field === 'toc_min_level' ? 2 : 4; 761 $current = $options[$field] ?? $default; 762 763 printf( 764 '<select id="%1$s" name="%2$s[%1$s]">', 765 esc_attr($field), 766 esc_attr(self::OPTION_GROUP), 767 ); 768 769 for ($level = 1; $level <= 6; $level++) { 770 printf( 771 '<option value="%d" %s>H%d</option>', 772 $level, 773 selected((int)$current, $level, false), 774 $level, 775 ); 776 } 777 778 echo '</select>'; 779 780 if (!empty($args['description'])) { 781 printf('<p class="description">%s</p>', esc_html($args['description'])); 782 } 783 } 784 785 /** 786 * Render TOC list type select dropdown. 787 * 788 * @param array<string, mixed> $args 789 */ 790 public function renderTocListTypeSelect(array $args): void 791 { 792 $options = get_option(self::OPTION_GROUP, []); 793 $field = $args['field']; 794 $current = $options[$field] ?? 'ul'; 795 796 $listTypes = [ 797 'ul' => __('Unordered (bullets)', 'djot-markup'), 798 'ol' => __('Ordered (numbers)', 'djot-markup'), 799 ]; 800 801 printf( 802 '<select id="%1$s" name="%2$s[%1$s]">', 803 esc_attr($field), 804 esc_attr(self::OPTION_GROUP), 805 ); 806 807 foreach ($listTypes as $value => $label) { 808 printf( 809 '<option value="%s" %s>%s</option>', 810 esc_attr($value), 811 selected($current, $value, false), 812 esc_html($label), 813 ); 814 } 815 816 echo '</select>'; 817 818 if (!empty($args['description'])) { 819 printf('<p class="description">%s</p>', esc_html($args['description'])); 820 } 821 } 560 822 } -
djot-markup/trunk/src/Converter.php
r3454148 r3454428 11 11 12 12 use Djot\DjotConverter; 13 use Djot\Extension\HeadingPermalinksExtension; 14 use Djot\Extension\SmartQuotesExtension; 15 use Djot\Extension\TableOfContentsExtension; 13 16 use Djot\Profile; 14 17 use Djot\Renderer\SoftBreakMode; … … 38 41 39 42 private bool $markdownMode; 43 44 private bool $tocEnabled; 45 46 private string $tocPosition; 47 48 private int $tocMinLevel; 49 50 private int $tocMaxLevel; 51 52 private string $tocListType; 53 54 private bool $permalinksEnabled; 55 56 private string $smartQuotesLocale; 40 57 41 58 /** … … 51 68 string $commentSoftBreak = 'newline', 52 69 bool $markdownMode = false, 70 bool $tocEnabled = false, 71 string $tocPosition = 'top', 72 int $tocMinLevel = 2, 73 int $tocMaxLevel = 4, 74 string $tocListType = 'ul', 75 bool $permalinksEnabled = false, 76 string $smartQuotesLocale = 'en', 53 77 ) { 54 78 $this->defaultSafeMode = $safeMode; … … 58 82 $this->commentSoftBreak = $commentSoftBreak; 59 83 $this->markdownMode = $markdownMode; 84 $this->tocEnabled = $tocEnabled; 85 $this->tocPosition = $tocPosition; 86 $this->tocMinLevel = $tocMinLevel; 87 $this->tocMaxLevel = $tocMaxLevel; 88 $this->tocListType = $tocListType; 89 $this->permalinksEnabled = $permalinksEnabled; 90 $this->smartQuotesLocale = $smartQuotesLocale; 60 91 $this->converter = new DjotConverter(safeMode: false); 61 92 $this->converter->getRenderer()->setCodeBlockTabWidth(4); … … 80 111 commentSoftBreak: $options['comment_soft_break'] ?? 'newline', 81 112 markdownMode: !empty($options['markdown_mode']), 113 tocEnabled: !empty($options['toc_enabled']), 114 tocPosition: $options['toc_position'] ?? 'top', 115 tocMinLevel: (int)($options['toc_min_level'] ?? 2), 116 tocMaxLevel: (int)($options['toc_max_level'] ?? 4), 117 tocListType: $options['toc_list_type'] ?? 'ul', 118 permalinksEnabled: !empty($options['permalinks_enabled']), 119 smartQuotesLocale: $options['smart_quotes_locale'] ?? 'en', 82 120 ); 83 121 } … … 93 131 { 94 132 $softBreakSetting = $context === 'comment' ? $this->commentSoftBreak : $this->postSoftBreak; 95 $key = $profileName . ($safeMode ? '_safe' : '_unsafe') . '_' . $softBreakSetting . ($this->markdownMode ? '_md' : ''); 133 $tocKey = ($this->tocEnabled && $context === 'article') 134 ? '_toc_' . $this->tocPosition . '_' . $this->tocMinLevel . '_' . $this->tocMaxLevel . '_' . $this->tocListType 135 : ''; 136 $permalinksKey = ($this->permalinksEnabled && $context === 'article') ? '_permalinks' : ''; 137 $smartQuotesKey = $this->smartQuotesLocale !== 'en' ? '_sq_' . $this->smartQuotesLocale : ''; 138 $key = $profileName . ($safeMode ? '_safe' : '_unsafe') . '_' . $softBreakSetting . ($this->markdownMode ? '_md' : '') . $tocKey . $permalinksKey . $smartQuotesKey; 96 139 97 140 if (!isset($this->profileConverters[$key])) { … … 123 166 // Convert tabs to 4 spaces in code blocks for consistent display 124 167 $converter->getRenderer()->setCodeBlockTabWidth(4); 168 169 // Add Table of Contents extension for articles when enabled 170 if ($this->tocEnabled && $context === 'article') { 171 $tocExtension = new TableOfContentsExtension( 172 minLevel: $this->tocMinLevel, 173 maxLevel: $this->tocMaxLevel, 174 listType: $this->tocListType, 175 cssClass: 'wpdjot-toc', 176 position: $this->tocPosition, 177 ); 178 $converter->addExtension($tocExtension); 179 180 // Wrap TOC in collapsible <details>/<summary> 181 $converter->addOutputTransformer(function (string $html): string { 182 $label = __('Table of Contents', 'djot-markup'); 183 184 return (string)preg_replace( 185 '#<nav class="wpdjot-toc">\n(.*?)</nav>\n#s', 186 '<details class="wpdjot-toc">' . "\n" 187 . '<summary>' . esc_html($label) . '</summary>' . "\n" 188 . '$1</details>' . "\n", 189 $html, 190 ); 191 }); 192 } 193 194 // Add heading permalinks for articles when enabled 195 if ($this->permalinksEnabled && $context === 'article') { 196 $converter->addExtension(new HeadingPermalinksExtension( 197 symbol: '#', 198 cssClass: 'wpdjot-permalink', 199 )); 200 } 201 202 // Add smart quotes extension for non-English locales 203 if ($this->smartQuotesLocale !== 'en') { 204 $locale = $this->smartQuotesLocale === 'auto' ? $this->getWpLocale() : $this->smartQuotesLocale; 205 $converter->addExtension(new SmartQuotesExtension(locale: $locale)); 206 } 125 207 126 208 // Allow customization via WordPress filters … … 367 449 return $allowedHtml; 368 450 } 451 452 /** 453 * Get the WordPress locale, falling back to 'en'. 454 */ 455 private function getWpLocale(): string 456 { 457 if (function_exists('get_locale')) { 458 return get_locale() ?: 'en'; 459 } 460 461 return 'en'; 462 } 369 463 } -
djot-markup/trunk/src/Plugin.php
r3454148 r3454428 491 491 'document.addEventListener("DOMContentLoaded", function() { hljs.highlightAll(); });', 492 492 ); 493 } 494 495 // Heading permalink copy-to-clipboard 496 if ($this->options['permalinks_enabled']) { 497 $copyJs = 'document.addEventListener("click",function(e){' 498 . 'var a=e.target.closest(".wpdjot-permalink");' 499 . 'if(!a)return;' 500 . 'e.preventDefault();' 501 . 'var hash=a.getAttribute("href");' 502 . 'history.replaceState(null,null,hash);' 503 . 'navigator.clipboard.writeText(location.href);' 504 . '});'; 505 wp_register_script('wpdjot-permalink', false, [], (string)WPDJOT_VERSION, ['in_footer' => true]); 506 wp_enqueue_script('wpdjot-permalink'); 507 wp_add_inline_script('wpdjot-permalink', $copyJs); 493 508 } 494 509 … … 533 548 'highlight_theme' => 'github', 534 549 'shortcode_tag' => 'djot', 550 'toc_enabled' => false, 551 'toc_position' => 'top', 552 'toc_min_level' => 2, 553 'toc_max_level' => 4, 554 'toc_list_type' => 'ul', 555 'permalinks_enabled' => false, 556 'smart_quotes_locale' => 'en', 535 557 ]; 536 558 -
djot-markup/trunk/vendor/autoload.php
r3454148 r3454428 20 20 require_once __DIR__ . '/composer/autoload_real.php'; 21 21 22 return ComposerAutoloaderInit 4fce3a77000f5adc49d1807e59eeac64::getLoader();22 return ComposerAutoloaderInit03e39cad62747481332e0a708bd351b8::getLoader(); -
djot-markup/trunk/vendor/composer/autoload_classmap.php
r3454148 r3454428 22 22 'Djot\\Extension\\HeadingPermalinksExtension' => $vendorDir . '/php-collective/djot/src/Extension/HeadingPermalinksExtension.php', 23 23 'Djot\\Extension\\MentionsExtension' => $vendorDir . '/php-collective/djot/src/Extension/MentionsExtension.php', 24 'Djot\\Extension\\SmartQuotesExtension' => $vendorDir . '/php-collective/djot/src/Extension/SmartQuotesExtension.php', 24 25 'Djot\\Extension\\TableOfContentsExtension' => $vendorDir . '/php-collective/djot/src/Extension/TableOfContentsExtension.php', 25 26 'Djot\\Filter\\ProfileFilter' => $vendorDir . '/php-collective/djot/src/Filter/ProfileFilter.php', … … 80 81 'Djot\\Profile' => $vendorDir . '/php-collective/djot/src/Profile.php', 81 82 'Djot\\ProfileViolation' => $vendorDir . '/php-collective/djot/src/ProfileViolation.php', 83 'Djot\\Renderer\\HeadingIdTracker' => $vendorDir . '/php-collective/djot/src/Renderer/HeadingIdTracker.php', 82 84 'Djot\\Renderer\\HtmlRenderer' => $vendorDir . '/php-collective/djot/src/Renderer/HtmlRenderer.php', 83 85 'Djot\\Renderer\\MarkdownRenderer' => $vendorDir . '/php-collective/djot/src/Renderer/MarkdownRenderer.php', … … 87 89 'Djot\\Renderer\\Utility\\EventDispatcherTrait' => $vendorDir . '/php-collective/djot/src/Renderer/Utility/EventDispatcherTrait.php', 88 90 'Djot\\SafeMode' => $vendorDir . '/php-collective/djot/src/SafeMode.php', 91 'WpDjot\\Admin\\Settings' => $baseDir . '/src/Admin/Settings.php', 92 'WpDjot\\Blocks\\DjotBlock' => $baseDir . '/src/Blocks/DjotBlock.php', 93 'WpDjot\\CLI\\MigrateCommand' => $baseDir . '/src/CLI/MigrateCommand.php', 94 'WpDjot\\Converter' => $baseDir . '/src/Converter.php', 95 'WpDjot\\Converter\\WpHtmlToDjot' => $baseDir . '/src/Converter/WpHtmlToDjot.php', 96 'WpDjot\\Converter\\WpMarkdownToDjot' => $baseDir . '/src/Converter/WpMarkdownToDjot.php', 97 'WpDjot\\Migration\\Migrator' => $baseDir . '/src/Migration/Migrator.php', 98 'WpDjot\\Plugin' => $baseDir . '/src/Plugin.php', 99 'WpDjot\\Shortcodes\\DjotShortcode' => $baseDir . '/src/Shortcodes/DjotShortcode.php', 89 100 ); -
djot-markup/trunk/vendor/composer/autoload_real.php
r3454148 r3454428 3 3 // autoload_real.php @generated by Composer 4 4 5 class ComposerAutoloaderInit 4fce3a77000f5adc49d1807e59eeac645 class ComposerAutoloaderInit03e39cad62747481332e0a708bd351b8 6 6 { 7 7 private static $loader; … … 25 25 require __DIR__ . '/platform_check.php'; 26 26 27 spl_autoload_register(array('ComposerAutoloaderInit 4fce3a77000f5adc49d1807e59eeac64', 'loadClassLoader'), true, true);27 spl_autoload_register(array('ComposerAutoloaderInit03e39cad62747481332e0a708bd351b8', 'loadClassLoader'), true, true); 28 28 self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); 29 spl_autoload_unregister(array('ComposerAutoloaderInit 4fce3a77000f5adc49d1807e59eeac64', 'loadClassLoader'));29 spl_autoload_unregister(array('ComposerAutoloaderInit03e39cad62747481332e0a708bd351b8', 'loadClassLoader')); 30 30 31 31 require __DIR__ . '/autoload_static.php'; 32 call_user_func(\Composer\Autoload\ComposerStaticInit 4fce3a77000f5adc49d1807e59eeac64::getInitializer($loader));32 call_user_func(\Composer\Autoload\ComposerStaticInit03e39cad62747481332e0a708bd351b8::getInitializer($loader)); 33 33 34 34 $loader->register(true); 35 35 36 $filesToLoad = \Composer\Autoload\ComposerStaticInit 4fce3a77000f5adc49d1807e59eeac64::$files;36 $filesToLoad = \Composer\Autoload\ComposerStaticInit03e39cad62747481332e0a708bd351b8::$files; 37 37 $requireFile = \Closure::bind(static function ($fileIdentifier, $file) { 38 38 if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) { -
djot-markup/trunk/vendor/composer/autoload_static.php
r3454148 r3454428 5 5 namespace Composer\Autoload; 6 6 7 class ComposerStaticInit 4fce3a77000f5adc49d1807e59eeac647 class ComposerStaticInit03e39cad62747481332e0a708bd351b8 8 8 { 9 9 public static $files = array ( … … 49 49 'Djot\\Extension\\HeadingPermalinksExtension' => __DIR__ . '/..' . '/php-collective/djot/src/Extension/HeadingPermalinksExtension.php', 50 50 'Djot\\Extension\\MentionsExtension' => __DIR__ . '/..' . '/php-collective/djot/src/Extension/MentionsExtension.php', 51 'Djot\\Extension\\SmartQuotesExtension' => __DIR__ . '/..' . '/php-collective/djot/src/Extension/SmartQuotesExtension.php', 51 52 'Djot\\Extension\\TableOfContentsExtension' => __DIR__ . '/..' . '/php-collective/djot/src/Extension/TableOfContentsExtension.php', 52 53 'Djot\\Filter\\ProfileFilter' => __DIR__ . '/..' . '/php-collective/djot/src/Filter/ProfileFilter.php', … … 107 108 'Djot\\Profile' => __DIR__ . '/..' . '/php-collective/djot/src/Profile.php', 108 109 'Djot\\ProfileViolation' => __DIR__ . '/..' . '/php-collective/djot/src/ProfileViolation.php', 110 'Djot\\Renderer\\HeadingIdTracker' => __DIR__ . '/..' . '/php-collective/djot/src/Renderer/HeadingIdTracker.php', 109 111 'Djot\\Renderer\\HtmlRenderer' => __DIR__ . '/..' . '/php-collective/djot/src/Renderer/HtmlRenderer.php', 110 112 'Djot\\Renderer\\MarkdownRenderer' => __DIR__ . '/..' . '/php-collective/djot/src/Renderer/MarkdownRenderer.php', … … 114 116 'Djot\\Renderer\\Utility\\EventDispatcherTrait' => __DIR__ . '/..' . '/php-collective/djot/src/Renderer/Utility/EventDispatcherTrait.php', 115 117 'Djot\\SafeMode' => __DIR__ . '/..' . '/php-collective/djot/src/SafeMode.php', 118 'WpDjot\\Admin\\Settings' => __DIR__ . '/../..' . '/src/Admin/Settings.php', 119 'WpDjot\\Blocks\\DjotBlock' => __DIR__ . '/../..' . '/src/Blocks/DjotBlock.php', 120 'WpDjot\\CLI\\MigrateCommand' => __DIR__ . '/../..' . '/src/CLI/MigrateCommand.php', 121 'WpDjot\\Converter' => __DIR__ . '/../..' . '/src/Converter.php', 122 'WpDjot\\Converter\\WpHtmlToDjot' => __DIR__ . '/../..' . '/src/Converter/WpHtmlToDjot.php', 123 'WpDjot\\Converter\\WpMarkdownToDjot' => __DIR__ . '/../..' . '/src/Converter/WpMarkdownToDjot.php', 124 'WpDjot\\Migration\\Migrator' => __DIR__ . '/../..' . '/src/Migration/Migrator.php', 125 'WpDjot\\Plugin' => __DIR__ . '/../..' . '/src/Plugin.php', 126 'WpDjot\\Shortcodes\\DjotShortcode' => __DIR__ . '/../..' . '/src/Shortcodes/DjotShortcode.php', 116 127 ); 117 128 … … 119 130 { 120 131 return \Closure::bind(function () use ($loader) { 121 $loader->prefixLengthsPsr4 = ComposerStaticInit 4fce3a77000f5adc49d1807e59eeac64::$prefixLengthsPsr4;122 $loader->prefixDirsPsr4 = ComposerStaticInit 4fce3a77000f5adc49d1807e59eeac64::$prefixDirsPsr4;123 $loader->classMap = ComposerStaticInit 4fce3a77000f5adc49d1807e59eeac64::$classMap;132 $loader->prefixLengthsPsr4 = ComposerStaticInit03e39cad62747481332e0a708bd351b8::$prefixLengthsPsr4; 133 $loader->prefixDirsPsr4 = ComposerStaticInit03e39cad62747481332e0a708bd351b8::$prefixDirsPsr4; 134 $loader->classMap = ComposerStaticInit03e39cad62747481332e0a708bd351b8::$classMap; 124 135 125 136 }, null, ClassLoader::class); -
djot-markup/trunk/vendor/composer/installed.json
r3454148 r3454428 3 3 { 4 4 "name": "php-collective/djot", 5 "version": "0.1.1 2",6 "version_normalized": "0.1.1 2.0",5 "version": "0.1.13", 6 "version_normalized": "0.1.13.0", 7 7 "source": { 8 8 "type": "git", 9 9 "url": "https://github.com/php-collective/djot-php.git", 10 "reference": " 0b8e06827b24e0447acfb6c79f6ff717b90440d1"10 "reference": "c79cba85d26483246e7d8b1bfdf387c0dad3d489" 11 11 }, 12 12 "dist": { 13 13 "type": "zip", 14 "url": "https://api.github.com/repos/php-collective/djot-php/zipball/ 0b8e06827b24e0447acfb6c79f6ff717b90440d1",15 "reference": " 0b8e06827b24e0447acfb6c79f6ff717b90440d1",14 "url": "https://api.github.com/repos/php-collective/djot-php/zipball/c79cba85d26483246e7d8b1bfdf387c0dad3d489", 15 "reference": "c79cba85d26483246e7d8b1bfdf387c0dad3d489", 16 16 "shasum": "" 17 17 }, … … 25 25 "phpunit/phpunit": "^11.0 || ^12.0" 26 26 }, 27 "time": "2026-0 1-20T05:36:44+00:00",27 "time": "2026-02-05T08:44:43+00:00", 28 28 "bin": [ 29 29 "bin/djot" … … 55 55 "support": { 56 56 "issues": "https://github.com/php-collective/djot-php/issues", 57 "source": "https://github.com/php-collective/djot-php/tree/0.1.1 2"57 "source": "https://github.com/php-collective/djot-php/tree/0.1.13" 58 58 }, 59 59 "funding": [ -
djot-markup/trunk/vendor/composer/installed.php
r3454148 r3454428 2 2 'root' => array( 3 3 'name' => 'php-collective/wp-djot', 4 'pretty_version' => '1. 0.0+no-version-set',5 'version' => '1. 0.0.0',6 'reference' => null,4 'pretty_version' => '1.4.0', 5 'version' => '1.4.0.0', 6 'reference' => '8557458d2d6cb8cc4b406577b26b2afc51868377', 7 7 'type' => 'wordpress-plugin', 8 8 'install_path' => __DIR__ . '/../../', … … 12 12 'versions' => array( 13 13 'php-collective/djot' => array( 14 'pretty_version' => '0.1.1 2',15 'version' => '0.1.1 2.0',16 'reference' => ' 0b8e06827b24e0447acfb6c79f6ff717b90440d1',14 'pretty_version' => '0.1.13', 15 'version' => '0.1.13.0', 16 'reference' => 'c79cba85d26483246e7d8b1bfdf387c0dad3d489', 17 17 'type' => 'library', 18 18 'install_path' => __DIR__ . '/../php-collective/djot', … … 21 21 ), 22 22 'php-collective/wp-djot' => array( 23 'pretty_version' => '1. 0.0+no-version-set',24 'version' => '1. 0.0.0',25 'reference' => null,23 'pretty_version' => '1.4.0', 24 'version' => '1.4.0.0', 25 'reference' => '8557458d2d6cb8cc4b406577b26b2afc51868377', 26 26 'type' => 'wordpress-plugin', 27 27 'install_path' => __DIR__ . '/../../', -
djot-markup/trunk/vendor/php-collective/djot/src/DjotConverter.php
r3454148 r3454428 10 10 use Djot\Node\Document; 11 11 use Djot\Parser\BlockParser; 12 use Djot\Renderer\HeadingIdTracker; 12 13 use Djot\Renderer\HtmlRenderer; 13 14 use Djot\Renderer\SoftBreakMode; … … 280 281 281 282 /** 283 * Get the heading ID tracker 284 */ 285 public function getHeadingIdTracker(): HeadingIdTracker 286 { 287 return $this->renderer->getHeadingIdTracker(); 288 } 289 290 /** 282 291 * Get the block parser for direct access 283 292 */ -
djot-markup/trunk/vendor/php-collective/djot/src/Extension/HeadingPermalinksExtension.php
r3454148 r3454428 8 8 use Djot\Event\RenderEvent; 9 9 use Djot\Node\Block\Heading; 10 use Djot\Node\Inline\HardBreak;11 10 use Djot\Node\Inline\Link; 12 use Djot\Node\Inline\SoftBreak;13 11 use Djot\Node\Inline\Span; 14 12 use Djot\Node\Inline\Text; 15 use Djot\Node\Node;16 13 17 14 /** … … 33 30 * ariaLabel: 'Link to this section', 34 31 * )); 32 * 33 * // Show on hover only, with copy-to-clipboard: 34 * $converter->addExtension(new HeadingPermalinksExtension( 35 * showOnHover: true, // adds 'permalink-hover' class to wrapper 36 * copyToClipboard: true, // adds 'data-permalink-copy' attribute to link 37 * )); 38 * ``` 39 * 40 * When `showOnHover` is enabled, add CSS like: 41 * ```css 42 * .permalink-hover { opacity: 0; transition: opacity .2s; } 43 * h1:hover > .permalink-hover, h2:hover > .permalink-hover, 44 * h3:hover > .permalink-hover, h4:hover > .permalink-hover, 45 * h5:hover > .permalink-hover, h6:hover > .permalink-hover, 46 * .permalink-hover:focus-within { opacity: 1; } 47 * ``` 48 * 49 * When `copyToClipboard` is enabled, add JavaScript like: 50 * ```javascript 51 * document.addEventListener('click', function(e) { 52 * var a = e.target.closest('[data-permalink-copy]'); 53 * if (!a) return; 54 * e.preventDefault(); 55 * navigator.clipboard.writeText(a.href); 56 * history.replaceState(null, null, a.getAttribute('href')); 57 * }); 35 58 * ``` 36 59 * … … 50 73 * @param string $ariaLabel Accessibility label for the link 51 74 * @param array<int> $levels Which heading levels to add permalinks to (1-6) 75 * @param bool $showOnHover Only show permalink on heading hover (injects CSS) 76 * @param bool $copyToClipboard Copy URL to clipboard on click instead of navigating (injects JS) 52 77 */ 53 78 public function __construct( … … 57 82 protected string $ariaLabel = 'Permalink', 58 83 protected array $levels = [1, 2, 3, 4, 5, 6], 84 protected bool $showOnHover = false, 85 protected bool $copyToClipboard = false, 59 86 ) { 60 87 } … … 62 89 public function register(DjotConverter $converter): void 63 90 { 64 $converter->on('render.heading', function (RenderEvent $event): void { 91 $tracker = $converter->getHeadingIdTracker(); 92 93 $converter->on('render.heading', function (RenderEvent $event) use ($tracker): void { 65 94 $node = $event->getNode(); 66 95 if (!$node instanceof Heading) { … … 72 101 } 73 102 74 // Get the ID (either explicit or will be generated by renderer) 75 /** @var string|null $id */ 76 $id = $node->getAttribute('id'); 77 if ($id === null || $id === '') { 78 // Generate ID from heading text (matching renderer logic) 79 $id = $this->generateIdFromNode($node); 80 // Set it explicitly so the renderer uses this ID, not one generated 81 // from the modified heading content (which would include the permalink symbol) 82 if ($id !== '') { 83 $node->setAttribute('id', $id); 84 } 85 } 103 // Get the deduplicated ID from the shared tracker 104 $id = $tracker->getIdForHeading($node); 86 105 87 106 if ($id === '') { 88 107 return; 108 } 109 110 // Set it explicitly so the renderer uses this ID, not one generated 111 // from the modified heading content (which would include the permalink symbol) 112 if (!$node->hasAttribute('id')) { 113 $node->setAttribute('id', $id); 89 114 } 90 115 … … 95 120 $link->appendChild(new Text($this->symbol)); 96 121 122 if ($this->copyToClipboard) { 123 $link->setAttribute('data-permalink-copy', ''); 124 } 125 97 126 // Wrap in span for styling flexibility 98 127 $span = new Span(); 99 128 $span->addClass('permalink-wrapper'); 129 if ($this->showOnHover) { 130 $span->addClass('permalink-hover'); 131 } 100 132 $span->appendChild($link); 101 133 102 134 // Add to heading 103 135 if ($this->position === 'before') { 104 // Prepend space + span, then the space105 136 $space = new Text(' '); 106 137 $node->prependChild($space); 107 138 $node->prependChild($span); 108 139 } else { 109 // Add space before110 140 $node->appendChild(new Text(' ')); 111 141 $node->appendChild($span); … … 113 143 }); 114 144 } 115 116 /**117 * Generate a slug from heading content (matches HtmlRenderer behavior)118 */119 protected function generateIdFromNode(Heading $node): string120 {121 $text = $this->getPlainText($node);122 if ($text === '') {123 return '';124 }125 126 // Match HtmlRenderer::getSectionId() behavior:127 // 1. Strip # characters entirely128 // 2. Trim whitespace129 // 3. Replace whitespace sequences with single dashes130 $id = str_replace('#', '', $text);131 $id = trim($id);132 $id = preg_replace('/[\s]+/', '-', $id) ?? $id;133 134 return $id;135 }136 137 /**138 * Extract plain text from a node and its children (recursive)139 */140 protected function getPlainText(Node $node): string141 {142 $text = '';143 foreach ($node->getChildren() as $child) {144 if ($child instanceof Text) {145 $text .= $child->getContent();146 } elseif ($child instanceof SoftBreak || $child instanceof HardBreak) {147 $text .= ' ';148 } elseif ($child instanceof Node) {149 $text .= $this->getPlainText($child);150 }151 }152 153 return $text;154 }155 145 } -
djot-markup/trunk/vendor/php-collective/djot/src/Extension/TableOfContentsExtension.php
r3454148 r3454428 7 7 use Djot\DjotConverter; 8 8 use Djot\Node\Block\Heading; 9 use Djot\Node\Inline\Text;10 use Djot\Node\Node;11 9 12 10 /** … … 75 73 public function register(DjotConverter $converter): void 76 74 { 75 $tracker = $converter->getHeadingIdTracker(); 76 77 77 // Hook into heading rendering to collect TOC entries 78 $converter->on('render.heading', function ($event) : void {78 $converter->on('render.heading', function ($event) use ($tracker): void { 79 79 $node = $event->getNode(); 80 80 if (!$node instanceof Heading) { … … 88 88 } 89 89 90 $text = $t his->getPlainText($node);91 $id = $t his->generateId($node, $text);90 $text = $tracker->getPlainText($node); 91 $id = $tracker->getIdForHeading($node); 92 92 93 93 $this->toc[] = [ … … 158 158 159 159 /** 160 * Extract plain text from a node161 */162 protected function getPlainText(Node $node): string163 {164 $text = '';165 foreach ($node->getChildren() as $child) {166 if ($child instanceof Text) {167 $text .= $child->getContent();168 } elseif (method_exists($child, 'getChildren')) {169 $text .= $this->getPlainText($child);170 }171 }172 173 return $text;174 }175 176 /**177 * Generate ID for a heading (matches HtmlRenderer::getSectionId() behavior)178 */179 protected function generateId(Heading $heading, string $text): string180 {181 // Check for explicit ID attribute182 $id = $heading->getAttribute('id');183 if (is_string($id) && $id !== '') {184 return $id;185 }186 187 // Generate from text - match HtmlRenderer::getSectionId() behavior:188 // 1. Strip # characters entirely189 // 2. Trim whitespace190 // 3. Replace whitespace sequences with single dashes191 if ($text === '') {192 return '';193 }194 195 $id = str_replace('#', '', $text);196 $id = trim($id);197 $id = preg_replace('/[\s]+/', '-', $id) ?? $id;198 199 return $id;200 }201 202 /**203 160 * Render TOC as nested HTML list 204 161 * -
djot-markup/trunk/vendor/php-collective/djot/src/Parser/InlineParser.php
r3454148 r3454428 65 65 protected ?array $cachedAbbreviations = null; 66 66 67 /** 68 * Smart quote characters (configurable via SmartQuotesExtension for locale support) 69 */ 70 protected string $openDoubleQuote = "\u{201C}"; 71 72 protected string $closeDoubleQuote = "\u{201D}"; 73 74 protected string $openSingleQuote = "\u{2018}"; 75 76 protected string $closeSingleQuote = "\u{2019}"; 77 78 /** 79 * Apostrophe character (always U+2019 RIGHT SINGLE QUOTATION MARK) 80 * 81 * Not configurable via extension — apostrophes are language-independent. 82 */ 83 protected string $apostrophe = "\u{2019}"; 84 67 85 public function __construct(protected BlockParser $blockParser) 68 86 { … … 117 135 { 118 136 return $this->customPatterns; 137 } 138 139 /** 140 * Set locale-specific smart quote characters 141 * 142 * Apostrophes (mid-word and before digits) always remain U+2019 143 * regardless of this setting. 144 */ 145 public function setQuoteCharacters( 146 string $openDoubleQuote, 147 string $closeDoubleQuote, 148 string $openSingleQuote, 149 string $closeSingleQuote, 150 ): void { 151 $this->openDoubleQuote = $openDoubleQuote; 152 $this->closeDoubleQuote = $closeDoubleQuote; 153 $this->openSingleQuote = $openSingleQuote; 154 $this->closeSingleQuote = $closeSingleQuote; 155 } 156 157 /** 158 * Get the current smart quote characters 159 * 160 * @return array{openDouble: string, closeDouble: string, openSingle: string, closeSingle: string} 161 */ 162 public function getQuoteCharacters(): array 163 { 164 return [ 165 'openDouble' => $this->openDoubleQuote, 166 'closeDouble' => $this->closeDoubleQuote, 167 'openSingle' => $this->openSingleQuote, 168 'closeSingle' => $this->closeSingleQuote, 169 ]; 119 170 } 120 171 … … 1055 1106 // Must be followed by closing } 1056 1107 if ($quotePos < $length && $text[$quotePos] === '}') { 1057 // Generate curlyquotes based on count1058 $openQuote = $marker === "'" ? "\u{2018}" : "\u{201C}";1059 $closeQuote = $marker === "'" ? "\u{2019}" : "\u{201D}";1108 // Generate quotes based on count 1109 $openQuote = $marker === "'" ? $this->openSingleQuote : $this->openDoubleQuote; 1110 $closeQuote = $marker === "'" ? $this->closeSingleQuote : $this->closeDoubleQuote; 1060 1111 1061 1112 // For pairs like {''}, output left + right 1062 // For single {'}, output just right (used for apostrophe)1113 // For single {'}, output apostrophe (always U+2019), {"} output close double 1063 1114 if ($quoteCount === 1) { 1064 $result = $ closeQuote;1115 $result = $marker === "'" ? $this->apostrophe : $closeQuote; 1065 1116 } elseif ($quoteCount === 2) { 1066 1117 $result = $openQuote . $closeQuote; … … 1122 1173 // Quote immediately after = is always an opener (attribute value start) 1123 1174 if ($prevChar === '=') { 1124 return $quote === '"' ? "\u{201C}" : "\u{2018}";1175 return $quote === '"' ? $this->openDoubleQuote : $this->openSingleQuote; 1125 1176 } 1126 1177 … … 1147 1198 // Single quote before digit is always apostrophe (e.g., '70s) 1148 1199 if ($quote === "'" && ctype_digit($nextChar)) { 1149 return "\u{2019}"; // closing/apostrophe1200 return $this->apostrophe; 1150 1201 } 1151 1202 1152 1203 // A quote after ] or ) cannot be an opener 1153 1204 if ($prevChar === ']' || $prevChar === ')') { 1154 return $quote === '"' ? "\u{201D}" : "\u{2019}";1205 return $quote === '"' ? $this->closeDoubleQuote : $this->closeSingleQuote; 1155 1206 } 1156 1207 1157 1208 if ($quote === '"') { 1158 1209 // Opening if preceded by space or start, closing otherwise 1159 return $prevIsSpace && !$nextIsSpace ? "\u{201C}" : "\u{201D}";1210 return $prevIsSpace && !$nextIsSpace ? $this->openDoubleQuote : $this->closeDoubleQuote; 1160 1211 } 1161 1212 … … 1166 1217 $matchingCloser = $this->findMatchingSingleQuoteCloser($text, $pos); 1167 1218 if ($matchingCloser !== null) { 1168 return "\u{2018}"; // opening quote1219 return $this->openSingleQuote; 1169 1220 } 1170 1221 1171 1222 // No matching closer found, treat as apostrophe 1172 return "\u{2019}"; 1173 } 1174 1175 // Closing/apostrophe 1176 return "\u{2019}"; 1223 return $this->apostrophe; 1224 } 1225 1226 // Check if this is mid-word (next char is a word character) — apostrophe 1227 if (preg_match('/\w/u', $nextChar)) { 1228 return $this->apostrophe; 1229 } 1230 1231 // Closing single quote 1232 return $this->closeSingleQuote; 1177 1233 } 1178 1234 … … 1359 1415 } 1360 1416 1361 // Check for multi-byte UTF-8 curly quotes (3 bytes each)1417 // Check for multi-byte configured quote characters 1362 1418 // These act as word boundaries for attribute attachment 1363 if (ord($char) >= 0x98 && ord($char) <= 0x9D && $wordStart >= 3) { 1364 $threeBytes = substr($textBuffer, $wordStart - 3, 3); 1365 // Check for curly quotes: " " ' ' (U+201C, U+201D, U+2018, U+2019) 1366 if ( 1367 $threeBytes === "\u{201C}" || $threeBytes === "\u{201D}" || 1368 $threeBytes === "\u{2018}" || $threeBytes === "\u{2019}" 1369 ) { 1370 break; 1419 foreach ($this->getConfiguredQuoteStrings() as $quoteStr) { 1420 $quoteLen = strlen($quoteStr); 1421 if ($wordStart >= $quoteLen && substr($textBuffer, $wordStart - $quoteLen, $quoteLen) === $quoteStr) { 1422 break 2; 1371 1423 } 1372 1424 } … … 1406 1458 'pos' => $attrEnd + 1, 1407 1459 ]; 1460 } 1461 1462 /** 1463 * Get all unique configured quote strings for word boundary detection 1464 * 1465 * @return array<string> 1466 */ 1467 protected function getConfiguredQuoteStrings(): array 1468 { 1469 return array_unique([ 1470 $this->openDoubleQuote, 1471 $this->closeDoubleQuote, 1472 $this->openSingleQuote, 1473 $this->closeSingleQuote, 1474 $this->apostrophe, 1475 ]); 1408 1476 } 1409 1477 -
djot-markup/trunk/vendor/php-collective/djot/src/Renderer/HtmlRenderer.php
r3454148 r3454428 71 71 protected array $footnoteRefCounts = []; 72 72 73 /** 74 * Tracks used IDs for deduplication 75 * 76 * @var array<string, int> 77 */ 78 protected array $usedIds = []; 79 80 /** 81 * Counter for auto-generated section IDs (when heading has no text) 82 */ 83 protected int $sectionCounter = 0; 73 protected HeadingIdTracker $headingIdTracker; 84 74 85 75 /** … … 116 106 public function __construct(protected bool $xhtml = false) 117 107 { 108 $this->headingIdTracker = new HeadingIdTracker(); 118 109 $this->initNodeRenderers(); 110 } 111 112 /** 113 * Get the heading ID tracker 114 */ 115 public function getHeadingIdTracker(): HeadingIdTracker 116 { 117 return $this->headingIdTracker; 119 118 } 120 119 … … 247 246 // Reset state for each render 248 247 $this->footnoteRefCounts = []; 249 $this->usedIds = []; 250 $this->sectionCounter = 0; 248 $this->headingIdTracker->reset(); 251 249 $this->footnoteNumbers = []; 252 250 $this->footnoteCounter = 0; … … 337 335 protected function getSectionId(Heading $node): string 338 336 { 339 // If heading has explicit id attribute, use it 337 return $this->headingIdTracker->getIdForHeading($node); 338 } 339 340 /** 341 * Track ID usage from non-heading elements (like paragraphs with explicit IDs) 342 */ 343 protected function trackIdFromNode(Node $node): void 344 { 340 345 if ($node->hasAttribute('id')) { 341 346 $idAttr = $node->getAttribute('id'); 342 347 $id = is_string($idAttr) ? $idAttr : ''; 343 // Track explicit IDs so auto-generated IDs don't conflict 344 if (!isset($this->usedIds[$id])) { 345 $this->usedIds[$id] = 0; 346 } 347 348 return $id; 349 } 350 351 // Generate from heading text 352 $headingText = $this->getPlainText($node); 353 354 if ($headingText === '') { 355 // Generate fallback ID 356 $this->sectionCounter++; 357 358 return 's-' . $this->sectionCounter; 359 } 360 361 // Convert to valid ID: 362 // 1. Strip # characters entirely 363 // 2. Trim whitespace 364 // 3. Replace whitespace sequences with single dashes 365 $baseId = str_replace('#', '', $headingText); 366 $baseId = trim($baseId); 367 $baseId = preg_replace('/[\s]+/', '-', $baseId) ?? $baseId; 368 369 // Track and deduplicate 370 if (!isset($this->usedIds[$baseId])) { 371 $this->usedIds[$baseId] = 0; 372 373 return $baseId; 374 } 375 376 // Already used, add suffix (first conflict is -1, second is -2, etc.) 377 $this->usedIds[$baseId]++; 378 379 return $baseId . '-' . $this->usedIds[$baseId]; 380 } 381 382 /** 383 * Track ID usage from non-heading elements (like paragraphs with explicit IDs) 384 */ 385 protected function trackIdFromNode(Node $node): void 386 { 387 if ($node->hasAttribute('id')) { 388 $idAttr = $node->getAttribute('id'); 389 $id = is_string($idAttr) ? $idAttr : ''; 390 if ($id !== '' && !isset($this->usedIds[$id])) { 391 $this->usedIds[$id] = 0; 392 } 348 $this->headingIdTracker->trackId($id); 393 349 } 394 350 } … … 516 472 protected function getPlainText(Node $node): string 517 473 { 518 $text = ''; 519 foreach ($node->getChildren() as $child) { 520 if ($child instanceof Text) { 521 $text .= $child->getContent(); 522 } elseif ($child instanceof SoftBreak || $child instanceof HardBreak) { 523 $text .= ' '; 524 } elseif ($child instanceof Node) { 525 $text .= $this->getPlainText($child); 526 } 527 } 528 529 return $text; 474 return $this->headingIdTracker->getPlainText($node); 530 475 } 531 476 -
djot-markup/trunk/wp-djot.php
r3454148 r3454428 2 2 /** 3 3 * Plugin Name: Djot Markup 4 * Plugin URI: https:// github.com/php-collective/wp-djot4 * Plugin URI: https://wordpress.org/plugins/djot-markup/ 5 5 * Description: <a href="https://djot.net/" target="_blank">Djot</a> markup language support for WordPress – a modern, cleaner alternative to Markdown with syntax highlighting. Convert Djot syntax to HTML in posts, pages, and comments. 6 * Version: 1. 3.16 * Version: 1.4.0 7 7 * Requires at least: 6.0 8 8 * Requires PHP: 8.2 … … 25 25 26 26 // Plugin constants 27 define('WPDJOT_VERSION', '1. 3.1');27 define('WPDJOT_VERSION', '1.4.0'); 28 28 define('WPDJOT_PLUGIN_DIR', plugin_dir_path(__FILE__)); 29 29 define('WPDJOT_PLUGIN_URL', plugin_dir_url(__FILE__));
Note: See TracChangeset
for help on using the changeset viewer.