diff --git a/CHANGELOG.md b/CHANGELOG.md index f5091bd..0e46e65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,28 @@ +# v1.4.0 +## 06/12/2017 + +1. [](#new) + * Added new `active` setting that supersedes `process` option + * Added `slug.granularity` option for slug generation + * Added and expose TOC generation (see [#8](https://github.com/Sommerregen/grav-plugin-toc/issues/8)) +2. [](#improved) + * Add a "headeranchor" class to the `hx` tag (thanks to [@Lamecarlate](https://github.com/Lamecarlate)) + * Moved template to new plugin template location + * Improved styling of code elements in table of contents + * Expose more plugin strings for translations + * Strip tags in title attributes + * Dropped `iconv` dependency +3. [](#bugfix) + * Fixed `{{ page.content|toc }}` not working [#8](https://github.com/Sommerregen/grav-plugin-toc/issues/8) + * Fixed issue with quote boxes that broke links in TOC [#10](https://github.com/Sommerregen/grav-plugin-toc/issues/10) + * Fixed error on multilingual site [#13](https://github.com/Sommerregen/grav-plugin-toc/issues/13) & [#14](https://github.com/Sommerregen/grav-plugin-toc/issues/14) + * Fixed undefined offset error in [MINITOC] [#12](https://github.com/Sommerregen/grav-plugin-toc/issues/12) & [#15](https://github.com/Sommerregen/grav-plugin-toc/issues/15) + # v1.3.1 ## 10/24/2015 2. [](#improved) - * Do not render TOC if it is empty. [#6](https://github.com/Sommerregen/grav-plugin-toc/issues/6) & [#7](https://github.com/Sommerregen/grav-plugin-toc/pull/7) + * Do not render TOC if it is empty [#6](https://github.com/Sommerregen/grav-plugin-toc/issues/6) & [#7](https://github.com/Sommerregen/grav-plugin-toc/pull/7) 3. [](#bugfix) * Fixed [#5](https://github.com/Sommerregen/grav-plugin-toc/pull/5) (Fix typo in `README.md`) diff --git a/LICENSE b/LICENSE index 51ea5a2..40deecb 100644 --- a/LICENSE +++ b/LICENSE @@ -25,7 +25,7 @@ using Grav Toc Plugin in any way. MIT LICENSE ----------- -Copyright (c) 2015 Benjamin Regler, https://github.com/sommerregen/grav-plugin-toc +Copyright (c) 2015-2017 Benjamin Regler, https://github.com/sommerregen/grav-plugin-toc Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 4ccb03c..e04ef7c 100644 --- a/README.md +++ b/README.md @@ -38,18 +38,19 @@ The `Toc` plugin comes with some sensible default configuration, that are pretty # Global plugin configurations enabled: true # Set to false to disable this plugin completely +active: true # Option to (de-)activate this plugin on a page built_in_css: true # Use built-in CSS of the plugin # Global and page specific configurations -title: true # Title to insert in the Table of Contents +title: true # Title to insert in the table of contents anchorlink: true # Set to true to cause all headers to link to themselves permalink: true # Set to true to generate permanent links at the beginning of each header placement: "left" # Either "left" or "right" visible: "hover" # Active on "hover" or "always" visible -icon: # Default link or a specific character like: #, ¶, ❡, and §. -class: # Adds the provided class to the anchor html +icon: "#" # Default link or a specific character like: #, ¶, ❡, and § +class: [] # Adds the provided classes to the anchor HTML baselevel: 1 # Base level for headings headinglevel: 6 # Maximum heading level to show in TOC @@ -58,9 +59,8 @@ slug: # Slug generation truncate: true # Truncate headings for slug generation length: 32 # Slug string length break: "-" # The break delimiter to divide the slug into pieces of words. - pad: "-..." # Added to the end of the truncated slug - -process: true # Process table of contents + pad: "..." # Added to the end of the truncated slug + level: "words" # Kind of slug generation (based on "words" or "character") ``` If you need to change any value, then the best process is to copy the [toc.yaml](toc.yaml) file into your `users/config/plugins/` folder (create it if it doesn't exist), and then modify there. This will override the default settings. @@ -126,7 +126,7 @@ Thanks! ## License -Copyright (c) 2015 [Benjamin Regler][github]. See also the list of [contributors] who participated in this project. +Copyright (c) 2015-2017 [Benjamin Regler][github]. See also the list of [contributors] who participated in this project. [Dual-licensed](LICENSE) for use under the terms of the [MIT][mit-license] or [GPLv3][gpl-license] licenses. diff --git a/assets/css/toc.css b/assets/css/toc.css index f95ec27..2a4ef30 100644 --- a/assets/css/toc.css +++ b/assets/css/toc.css @@ -1,115 +1,117 @@ /* General styles */ .table-of-contents { - font-size: .8em; - display: inline-block; + font-size: .8em; + display: inline-block; } .table-of-contents.toc, .table-of-contents.minitoc { - background-color: #F8F8F8; - box-shadow: 0 0 5px 0 rgba(50, 50, 50, 0.4); - padding: 1em 2em; - min-width: 33.3%; - max-width: 25%; + background-color: #F8F8F8; + box-shadow: 0 0 5px 0 rgba(50, 50, 50, 0.4); + padding: 1em 2em; + min-width: 33.3%; + max-width: 50%; } .table-of-contents.toc { - float: right; - margin: 0 0 2em 2em; + float: right; + margin: 0 0 2em 2em; } /* TOC elements */ .table-of-contents .toctitle { - display: block; - font-size: larger; - font-weight: bold; - margin-bottom: .5em; + display: block; + font-size: larger; + font-weight: bold; + margin-bottom: .5em; } .table-of-contents ul { - list-style: none; - padding: 0; - margin: 0; + list-style: none; + padding: 0; + margin: 0; } .table-of-contents > ul { - margin-left: 1.5em; + margin-left: 1.5em; } .table-of-contents ul ul { - padding: 0 1em; + padding: 0 1em; } .table-of-contents li { - margin: 0 0 .25em; - display: block; - color: #808080; + margin: 0 0 .25em; + display: block; + color: #808080; } .table-of-contents .toclink:before { - content: "\00BB"; /* » */ - display: inline; - margin-right: 0.5ex; + content: "\00BB"; /* » */ + display: inline; + margin-right: 0.5ex; } .toclink { - margin-left: -1em; + margin-left: -1em; } /* Mini TOC */ .table-of-contents.minitoc { - margin: -1em 0; + margin: -1em 0; } /* Blog adjustments */ .list-item { - clear: right; - display: inline-block; - width: 100%; + clear: right; + display: inline-block; + width: 100%; } /* Anchorlinks */ .headeranchor-link { - color: inherit; - display: inline-block; - outline: none; - position: relative; + color: inherit; + display: inline-block; + outline: none; + position: relative; } .headeranchor-link:before, .headeranchor-link-left:before { - content: attr(data-icon); /* "\2693" Dingsbat anchor */ - display: inline-block; - margin-left: -1em; - padding-right: .3em; - text-align: right; - text-decoration: none; - width: 1em; + content: attr(data-icon); /* "\2693" Dingsbat anchor */ + display: inline-block; + margin-left: -1em; + padding-right: .3em; + text-align: right; + text-decoration: none; + width: 1em; - position: relative; - left: -9999px; - height: 0; - opacity: 0; - visibility: hidden; + position: relative; + left: -9999px; + height: 0; + opacity: 0; + visibility: hidden; - transition: opacity .4s ease-in-out 0s + transition: opacity .4s ease-in-out 0s } .headeranchor-link:hover:before, -.headeranchor-link.headeranchor-visible-always:before { - left: 0; - height: auto; - opacity: 1; - visibility: visible; +.headeranchor-link.headeranchor-visible--always:before { + left: 0; + height: auto; + opacity: 1; + visibility: visible; } -.headeranchor-link-right:hover:before, -.headeranchor-link-right.headeranchor-visible-always:before { - left: calc(100% + 1.1em); +.headeranchor-link--right:hover:before, +.headeranchor-link--right.headeranchor-visible--always:before { + left: calc(100% + 1.1em); } .headeranchor-link.no-anchor:hover:before { - left: -9999px; - height: 0; - opacity: 0; - visibility: hidden; + left: -9999px; + height: 0; + opacity: 0; + visibility: hidden; } /* Toc links */ .toclink code, .headeranchor-link code { - font-size: inherit; - vertical-align: inherit; + background: none; + color: inherit; + font-size: inherit; + vertical-align: inherit; } .toclink:hover code, .headeranchor-link:hover code { - color: inherit; + color: inherit; } diff --git a/blueprints.yaml b/blueprints.yaml index fcc5432..ba4cecc 100644 --- a/blueprints.yaml +++ b/blueprints.yaml @@ -1,18 +1,19 @@ name: "Toc" -version: 1.3.1 +version: "1.4.0" description: "This plugin automagically generates a (minified) Table of Contents based on special markers in the document and adds it into the resulting HTML document." -icon: language +icon: "language" author: - name: Sommerregen - email: sommerregen@benjamin-regler.de -homepage: https://github.com/sommerregen/grav-plugin-toc -keywords: [toc, filter, formatter, plugin] -docs: https://github.com/sommerregen/grav-plugin-toc/blob/master/README.md -bugs: https://github.com/sommerregen/grav-plugin-toc/issues -license: MIT/GPL + name: "Sommerregen" + email: "sommerregen@benjamin-regler.de" +homepage: "https://github.com/sommerregen/grav-plugin-toc" +keywords: ["toc", "filter", "formatter", "plugin"] +docs: "https://github.com/sommerregen/grav-plugin-toc/blob/master/README.md" +bugs: "https://github.com/sommerregen/grav-plugin-toc/issues" +license: "MIT/GPL" form: validation: strict + fields: global: type: section @@ -31,6 +32,18 @@ form: validate: type: bool + active: + type: toggle + label: PLUGINS.TOC.PLUGIN_ACTIVE + help: PLUGINS.TOC.PLUGIN_ACTIVE_HELP + highlight: 1 + default: 1 + options: + 1: PLUGIN_ADMIN.YES + 0: PLUGIN_ADMIN.NO + validate: + type: bool + built_in_css: type: toggle label: PLUGINS.TOC.BUILTIN_CSS @@ -51,6 +64,7 @@ form: title: type: toggle label: PLUGINS.TOC.TITLE + highlight: 1 default: 1 options: 1: PLUGIN_ADMIN.YES @@ -62,6 +76,7 @@ form: type: toggle label: PLUGINS.TOC.ANCHORLINK help: PLUGINS.TOC.ANCHORLINK_HELP + highlight: 1 default: 1 options: 1: PLUGIN_ADMIN.YES @@ -73,6 +88,7 @@ form: type: toggle label: PLUGINS.TOC.PERMALINK help: PLUGINS.TOC.PERMALINK_HELP + highlight: 1 default: 1 options: 1: PLUGIN_ADMIN.YES @@ -136,17 +152,6 @@ form: min: 1 max: 6 - process: - type: toggle - label: PLUGINS.TOC.PROCESS - highlight: 1 - default: 1 - options: - 1: PLUGIN_ADMIN.YES - 0: PLUGIN_ADMIN.NO - validate: - type: bool - slug: type: section title: PLUGINS.TOC.SLUG.SECTION @@ -155,6 +160,7 @@ form: slug.truncate: type: toggle label: PLUGINS.TOC.SLUG.TRUNCATE + highlight: 1 default: 1 options: 1: PLUGIN_ADMIN.YES @@ -162,6 +168,17 @@ form: validate: type: bool + slug.granularity: + type: select + size: small + label: PLUGINS.TOC.SLUG.GRANULARITY.LABEL + default: words + options: + words: PLUGINS.TOC.SLUG.GRANULARITY.WORDS + characters: PLUGINS.TOC.SLUG.GRANULARITY.CHARACTERS + validate: + type: string + slug.length: type: text size: x-small diff --git a/classes/Toc.php b/classes/Toc.php index 4be1859..d3de6f6 100644 --- a/classes/Toc.php +++ b/classes/Toc.php @@ -10,504 +10,484 @@ namespace Grav\Plugin; +use Grav\Common\Grav; use ForceUTF8\Encoding; -use Grav\Common\GravTrait; use RocketTheme\Toolbox\Event\Event; /** * Toc * - * Helper class to automagically generatea a (minified) Table of Contents + * Helper class to automagically generate a (minified) Table of Contents * based on special markers in the document and adds it into the * resulting HTML document. */ class Toc { - /** - * @var Toc - */ - use GravTrait; - - /** - * Current language of the document - * - * @var string - */ - protected $language; - - /** - * Current options of the page - * - * @var Grav\Common\Data\Data - */ - protected $options; - - /** --------------------------- - * Private/protected properties - * ---------------------------- - */ - - /** - * Regex for Markdown (setext-style and atx-style headers): - * ~^(?P\#{1,6})?[ ]* - * (?P.+?)(?(1)\#*|[ ]*\n(=+|-+)[ ]*)\n+~m'; - * - * @var string - */ - protected $regex = [ - 'html' => '~<(?Ppre|code|blockquote|q|cite|h\d+)\s*(?P[^>]*)>(?P.*?)~ims', - 'markdown' => "~^(?P\#{1,6})?[ ]*(?P.+?)(?(1)\#*|[ ]*\n(=+|-+)[ ]*)\n+~m" - ]; - - /** ------------- - * Public methods - * -------------- - */ - - /** - * Constructor. - */ - public function __construct() - { - // Load ForceUTF8 package - require_once(dirname(__DIR__) . '/vendor/ForceUTF8/src/ForceUTF8/Encoding.php'); - } - - /** - * Create and link the table of contents at the top of the file. - * - * @param string $content The content to be use for creating the TOC - * - * @return array Returns an array of headings in the format: - * $offset => [ - * 'tag' => ..., 'level' => ..., - * 'text' => ..., 'id' => ... - * ] - */ - public function createToc($content, $origin = 'html') - { - $toc = []; - $counter = []; - - if (preg_match_all($this->regex[$origin], $content, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { - foreach ($matches as $match) { - $offset = $match[0][1]; - $tag = strtolower($match['tag'][0]); - - if ($origin == 'markdown') { - $tag = 'h'.strlen($tag); - // Don't consider headings in code or pre or blockquote environments - } elseif ($tag{0} !== 'h') { - continue; + /** + * Regex for Markdown (setext-style and atx-style headers): + * ~^(?P\#{1,6})?[ ]* + * (?P.+?)(?(1)\#*|[ ]*\n(=+|-+)[ ]*)\n+~m'; + * + * @var string + */ + protected $regex = [ + 'html' => '~<(?Ppre|code|blockquote|q|cite|h\d+)\s*(?P[^>]*)>(?P.*?)~ims', + 'markdown' => "~^(?P\#{1,6})?[ ]*(?P.+?)(?(1)\#*|[ ]*\n(=+|-+)[ ]*)\n+~m" + ]; + + /** + * Create and link the table of contents at the top of the file. + * + * @param string $content The content to be use for creating the TOC + * + * @return array Returns an array of headings in the format: + * $offset => [ + * 'tag' => ..., 'level' => ..., + * 'text' => ..., 'id' => ... + * ] + */ + public function createToc($content, $options = [], $origin = 'html') + { + $toc = []; + $counter = []; + + if (preg_match_all($this->regex[$origin], $content, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) { + $language = $options->get('language'); + + foreach ($matches as $match) { + $offset = $match[0][1]; + + $tag = strtolower($match['tag'][0]); + $text = trim($match['text'][0]); + + if ($origin == 'markdown') { + $tag = 'h' . strlen($tag); + } + + // Ignore headings in code, pre or blockquote environments + if (!$text || $tag{0} !== 'h') { + continue; + } + + // Extract information from HTML tag + $level = (int) mb_substr($tag, 1); + + // Expand tag attributes + $attributes = $this->parseAttributes($match['attr'][0]); + $id = isset($attributes['id']) ? $attributes['id'] : $this->hyphenize($text, $options, $language); + + // Replace empty id with hash of text + if (strlen($id) == 0) { + $id = substr(md5($text), 0, 6); + } + + if (isset($counter[$id])) { + $id = $id . '-' . $counter[$id]++; + } else { + $counter[$id] = 1; + } + + // Prevent TOC and MINITOC insertion in headings + $text = str_ireplace(['[TOC]', '[MINITOC]'], + ['[TOC]', '[MINITOC]'], $text); + + $toc[$offset] = [ + 'tag' => $tag, + 'level' => $level, + 'indent' => $level - 1, + 'text' => $text, + 'id' => $id, + ]; + } } - // Extract informations from HTML tags - $level = (int) mb_substr($tag, 1); - $text = trim($match['text'][0]); - if (empty($text)) { - continue; - } + // Create tree of headings and their levels + return $this->mapTree($toc); + } - // Expand tag attributes - $attributes = $this->parseAttributes($match['attr'][0]); - $id = isset($attributes['id']) ? $attributes['id'] : $this->hyphenize($text); + /** + * Tocify content, i.e. insert anchor- and permalinks into headings. + * + * @param string $content The content to be tocified + * @param array $options Array of options for the TOC filter + * + * @return string The content with inserted anchor- and + * permalinks in headings + */ + public function tocify($content, $options = []) + { + // Change regex, i.e. allow headers in (block-)quotes being parsed + $regex = str_replace('blockquote|q|cite|', '', $this->regex['html']); + + $counter = []; + $content = preg_replace_callback($regex, + function($match) use ($options, &$counter) { + $tag = strtolower($match['tag']); + $text = trim($match['text']); + + // Don't consider headings in code or pre environments + if (($tag{0} !== 'h') || (mb_strlen($text) == 0)) { + // Ignore empty headers, too + return $match[0]; + } + + // Extract informations from HTML tags + $level = $indent = (int) mb_substr($tag, 1); + + // Expand tag attributes + $language = $options->get('language'); + $attributes = $this->parseAttributes($match['attr']); + $id = isset($attributes['id']) ? $attributes['id'] : $this->hyphenize($text, $options, $language); + + $classes = isset($attributes['class']) ? $attributes['class'] . ' headeranchor' : 'headeranchor'; + + // Replace empty id with hash of text + if (strlen($id) == 0) { + $id = substr(md5($text), 0, 6); + } + + // Increment counter on same heading names + if (isset($counter[$id])) { + $id = $id . '-' . $counter[$id]++; + } else { + $counter[$id] = 1; + } + + // Add permalink + if ($options->get('permalink')) { + $placement = $options->get('placement', 'left'); + $visible = $options->get('visible', 'hover'); + + // Compile custom configurations for header link + $extra = ' headeranchor-link--' . $placement; + $extra .= ' headeranchor-visible--' . $visible; + + // Compile default classes + $default_classes = $options->get('class', []); + if (is_array($default_classes)) { + $default_classes = implode(' ', $default_classes); + } + + $extra .= ' ' . ltrim(implode(' ', [$options->get('hover', ''), $default_classes])); + + // Load header anchor link icon + $icon = $options->get('icon', '#'); + + $text = sprintf('', $id, strip_tags($text), $text, rtrim($extra), $icon); + } + + // Add id attribute (and a "headeranchor" class) if permalinks or anchorlinks are used + $link = $options->get('anchorlink', $options->get('permalink')); + $attributes += $link ? ['id' => $id] : []; + if ($link) { + $attributes['class'] = $classes; + } + + // Prevent TOC and MINITOC insertion in headings + $text = str_ireplace(['[TOC]', '[MINITOC]'], ['[TOC]', '[MINITOC]'], $text); + + // Stringify HTML attributes + $attributes = $this->htmlAttributes($attributes); + + // Return tag with its text content + return "<$tag$attributes>$text"; + }, $content); + + return $content; + } - // Replace empty id with hash of text - if (strlen($id) == 0) { - $id = substr(md5($text), 0, 6); + /** + * Process contents i.e. apply TOC filer to the content. + * + * @param string $content The content to render + * @param array $options Options to be passed to the renderer + * @param null|Page $page Null or a page instance + * + * @return string The rendered contents. + */ + public function render($content, $options = [], $page = null) + { + /** @var Twig $twig */ + $twig = Grav::instance()['twig']; + + // Save current user language + $options->set('language', $page->language()); + + // Generate Toc + $replacements = $this->generateToc($content, $options); + $regex = '~(

)?\s*\[(?P(?:MINI)?TOC)\]\s*(?(1)

)~i'; + + if (!$replacements) { + // Hide (mini-)toc marker + return preg_replace($regex, '', $content); } - if (isset($counter[$id])) { - $id = $id.'-'.$counter[$id]++; - } else { - $counter[$id] = 1; - } + // Tocify content + $content = $this->tocify($content, $options); + + // Replace TOC and MINITOC placeholders + $content = preg_replace_callback($regex, + function($match) use ($replacements, $twig, $options) { + static $i = 0; - // Prevent TOC and MINITOC insertion in headings - $text = str_ireplace(['[TOC]', '[MINITOC]'], - ['[TOC]', '[MINITOC]'], $text); - - $toc[$offset] = [ - 'tag' => $tag, - 'level' => $level, - 'indent' => $level - 1, - 'text' => $text, - 'id' => $id, - ]; - } + $vars['toc'] = $replacements[$i++] + $options->toArray(); + $template = 'plugins/toc/toc' . TEMPLATE_EXT; + return $twig->processTemplate($template, $vars); + }, $content); + + // Return modified content + return $content; } - // Create tree of headings and their levels - return $this->mapTree($toc); - } - - /** - * Tocify content, i.e. insert anchor- and permalinks into headings. - * - * @param string $content The content to be tocified - * @param array $options Array of options for the TOC filter - * - * @return string The content with inserted anchor- and - * permalinks in headings - */ - public function tocify($content, $options = []) - { - // Change regex, i.e. allow headers in (block-)quotes being parsed - $regex = str_replace('blockquote|q|cite|', '', $this->regex['html']); - - $counter = []; - $content = preg_replace_callback($regex, - function($match) use ($options, &$counter) { - $tag = strtolower($match['tag']); - $text = trim($match['text']); - - // Don't consider headings in code or pre environments - if (($tag{0} !== 'h') || (mb_strlen($text) == 0)) { - // Ignore empty headers, too - return $match[0]; + /** + * Generate a TOC of a given document. + * + * @param string $content The content the TOC has to be generated for + * @param array $options A list of options + * + * @return array An array of TOCs + */ + public function generateToc($content, $options = []) + { + $replacements = []; + // Find all occurrences of TOC and MINITOC in content + $regex = '~(

)?\s*\[(?P(?:MINI)?TOC)\]\s*(?(1)

)~i'; + if (preg_match_all($regex, $content, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER) === false) { + return $content; } - // Extract informations from HTML tags - $level = $indent = (int) mb_substr($tag, 1); + // Generate TOC + $toc = $this->createToc($content, $options); + if (!$toc) { + // Hide (mini-)toc marker + return []; + } - // Expand tag attributes - $attributes = $this->parseAttributes($match['attr']); - $id = isset($attributes['id']) ? $attributes['id'] : $this->hyphenize($text); + foreach ($matches as $match) { + $offset = $match[0][1]; + $type = strtolower($match['type'][0]); - // Replace empty id with hash of text - if (strlen($id) == 0) { - $id = substr(md5($text), 0, 6); - } + // Initialize variables + $current = -1; + $minitoc = []; - // Increment counter on same heading names - if (isset($counter[$id])) { - $id = $id.'-'.$counter[$id]++; - } else { - $counter[$id] = 1; - } + if ($type == 'toc') { + $minitoc = $toc; + } else { + // Get current (sub-)heading + foreach ($toc as $index => $heading) { + if ($index < $offset) { + $current = $index; + } else { + $level = ($current > -1) ? $toc[$current]['level'] : -1; + if ($heading['level'] > $level) { + $minitoc[$index] = $heading; + } else { + break; + } + } + } + } - // Add permalink - if ($options->get('permalink')) { - // Compile custom configurations for header link - $extra = ' '; - if ('left' !== ($placement = $options->get('placement', ' '))) { - $extra .= 'headeranchor-link-' . $placement . ' '; - } - if ('hover' !== ($visible = $options->get('visible', ' '))) { - $extra .= 'headeranchor-visible-' . $visible . ' '; - } - $extra .= implode(' ', [ - $options->get('hover', ''), - implode(' ', $options->get('class', []))]); - - // Load header anchor link icon - $icon = $options->get('icon', '#'); - - $text = sprintf('', - $id, strip_tags($text), $text, rtrim($extra), $icon); + // Save rendered TOC for later replacement + $replacements[] = [ + 'list' => $minitoc, + 'type' => $type, + 'heading' => ($current > -1) ? $toc[$current] : null, + ]; } - // Add id attribute if permalinks or anchorlinks are used - $link = $options->get('anchorlink', $options->get('permalink')); - $attributes += $link ? ['id' => $id] : []; - - // Prevent TOC and MINITOC insertion in headings - $text = str_ireplace(['[TOC]', '[MINITOC]'], - ['[TOC]', '[MINITOC]'], $text); - - // Stringify HTML attributes - $attributes = $this->htmlAttributes($attributes); - - // Return tag with its text content - return "<$tag$attributes>$text"; - }, $content); - - return $content; - } - - /** - * Process contents i.e. apply TOC filer to the content. - * - * @param string $content The content to render. - * @param array $options Options to be passed to the renderer. - * @param null|Page $page Null or an instance of \Grav\Common\Page. - * - * @return string The rendered contents. - */ - public function render($content, $options = [], $page = null) - { - /** @var Twig $twig */ - $twig = self::getGrav()['twig']; - - // Save current user language - $this->language = $page->language() ? [$page->language()] : null; - $this->options = $options; - - $replacements = []; - // Find all occurrences of TOC and MINITOC in content - $regex = '~(

)?\s*\[(?P(?:MINI)?TOC)\]\s*(?(1)

)~i'; - if (preg_match_all($regex, $content, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER) === false) { - return $content; + return $replacements; } - // Generate TOC - $toc = $this->createToc($content); - if (empty($toc)) { - // Hide (mini-)toc marker - return preg_replace($regex, '', $content); - } + /** + * Map a list of headings to a flattened tree. + * + * @param array $list A list with headings + * @return array A flattened tree of $list + */ + protected function mapTree(array $list) + { + static $indent = -1; + + if (!$list) { + return $list; + } - foreach ($matches as $match) { - $offset = $match[0][1]; - $type = strtolower($match['type'][0]); - - // Initialize variables - $current = -1; - $minitoc = []; - - if ($type == 'toc') { - $minitoc = $toc; - } else { - // Get current (sub-)heading - foreach ($toc as $index => $heading) { - if ($index < $offset) { - $current = $index; - } else { - $level = $toc[$current]['level']; - if ($heading['level'] > $level) { - $minitoc[$index] = $heading; - } else { - break; + // Adjust TOC indentation based on baselevel + $baselevel = min(array_map(function($elem) { + return $elem['level']; + }, $list)); + + $toc = []; + $subtoc = []; + $indent++; + + // Create Toc tree + foreach ($list as $offset => $heading) { + if ($heading['level'] == $baselevel) { + if (count($subtoc)) { + $toc += $this->mapTree($subtoc); + $subtoc = []; + } + + $heading['indent'] = (int) $indent; + $toc[$offset] = $heading; + } elseif ($heading['level'] > $baselevel) { + $subtoc[$offset] = $heading; } - } } - } - // Render TOC - $vars['toc'] = [ - 'list' => $minitoc, - 'type' => $type, - 'heading' => ($current > -1) ? $toc[$current] : null, - ] + $options->toArray(); + if (count($subtoc) > 0) { + $toc += $this->mapTree($subtoc); + } - $template = 'partials/toc' . TEMPLATE_EXT; - $minitoc = $twig->processTemplate($template, $vars); + $indent--; + return $toc; + } - // Save rendered TOC for later replacement - $replacements[] = $minitoc; + /** + * Parse HTML attributes from a tag. + * + * @param string $text The attributes from a HTML tag as a string. + * + * @return array Returns the parsed attributes as an indexed + * array + */ + protected function parseAttributes($text) + { + $attributes = []; + $pattern = '~(?(DEFINE) + (?[a-zA-Z][a-zA-Z0-9-:]*) + (?"[^"]+") + (?\'[^\']+\') + (?[^\s>]+) + (?((?&value_double)|(?&value_single)|(?&value_none))) + ) + (?(?&name))(=(?(?&value)))?~xs'; + + if (preg_match_all($pattern, $text, $matches, PREG_SET_ORDER)) { + foreach ($matches as $match) { + $attributes[$match['n']] = isset($match['v']) ? trim($match['v'], '\'"') : null; + } + } + return $attributes; } - // Tocify content - $content = $this->tocify($content, $options); - $this->language = null; - - // Replace TOC and MINITOC placeholders - $content = preg_replace_callback($regex, - function($match) use ($replacements) { - static $i = 0; - return $replacements[$i++]; - }, $content); - - // Return modified content - return $content; - } - - /** ------------------------------- - * Private/protected helper methods - * -------------------------------- - */ - - /** - * Map a list of headings to a flattened tree. - * - * @param array $list A list with headings - * @return array A flattened tree of the $list. - */ - protected function mapTree(array $list) - { - static $indent = -1; - - if (empty($list)) { - return $list; + /** + * Convert an array of attributes into its HTML representation. + * + * @param array $attributes The attributes to be converted to a + * HTML string + * + * @return string The converted attributes + */ + protected function htmlAttributes(array $attributes = []) + { + foreach ($attributes as $attribute => &$data) { + $data = implode(' ', (array) $data); + $data = $attribute . '="' . htmlspecialchars($data, ENT_QUOTES, 'UTF-8') . '"'; + } + return $attributes ? ' ' . implode(' ', $attributes) : ''; } - // Adjust TOC indentation based on baselevel - $baselevel = min(array_map(function($elem) { - return $elem['level']; - }, $list)); - - $toc = []; - $subtoc = []; - $indent++; - - // Create Toc tree - foreach ($list as $offset => $heading) { - if ($heading['level'] == $baselevel) { - if (count($subtoc)) { - $toc += $this->mapTree($subtoc); - $subtoc = []; + /** + * Converts a word "into-it-s-hyphenated-version" (UTF-8 safe). + * + * A hyphenated word must begin with a letter ([A-Za-z]) and may be + * followed by any number of letters, digits ([0-9]), hyphens ("-"), + * underscores ("_"), colons (":"), and periods ("."). + * + * @param string $word Word to hyphenate + * @param array $options A list of options + * @param array $language A language code used to convert the word + * + * @return string The hyphenated word + */ + protected function hyphenize($word, $options = [], $language = null) + { + /** @var Grav\Common\Language\Language $l */ + $l = Grav::instance()['language']; + + // Set locale for transliterating Unicode text to plain ASCII text + $locale = setlocale(LC_CTYPE, 0); + setlocale(LC_CTYPE, 'en_US.UTF8'); + + // Ensure word is UTF-8 encoded + $text = html_entity_decode($word, ENT_COMPAT, 'UTF-8'); + + // Strip tags + $text = strip_tags($text); + + // Perform some language dependent replacements + $lang = $language ?: $l->getLanguage(); + $replacements = $l->translate('PLUGINS.TOC.PATTERNS', [$lang], true); + if (is_array($replacements)) { + $text = preg_replace(array_keys($replacements), $replacements, $text); } - $heading['indent'] = (int) $indent; - $toc[$offset] = $heading; - } elseif ($heading['level'] > $baselevel) { - $subtoc[$offset] = $heading; - } - } + // Trim and transliterate + $text = Encoding::toLatin1(Encoding::toUTF8(trim($text, '-'))); - if (count($subtoc)) { - $toc += $this->mapTree($subtoc); - } + // Lowercase + $text = strtolower($text); - $indent--; - return $toc; - } - - /** - * Parse HTML attributes from a tag. - * - * @param string $text The attributes from a HTML tag as a string. - * - * @return array Returns the parsed attributes as an indexed - * array - */ - protected function parseAttributes($text) - { - $attributes = []; - $pattern = '~(?(DEFINE) - (?[a-zA-Z][a-zA-Z0-9-:]*) - (?"[^"]+") - (?\'[^\']+\') - (?[^\s>]+) - (?((?&value_double)|(?&value_single)|(?&value_none))) - ) - (?(?&name))(=(?(?&value)))?~xs'; - - if (preg_match_all($pattern, $text, $matches, PREG_SET_ORDER)) { - foreach ($matches as $match) { - $attributes[$match['n']] = isset($match['v']) - ? trim($match['v'], '\'"') - : null; - } - } - return $attributes; - } - - /** - * Convert an array of attributes into its HTML representation. - * - * @param array $attributes The attributes to be converted to a - * HTML string - * - * @return string The converted attributes - */ - protected function htmlAttributes(array $attributes = []) - { - foreach ($attributes as $attribute => &$data) { - $data = implode(' ', (array) $data); - $data = $attribute.'="'.htmlspecialchars($data, ENT_QUOTES, 'UTF-8').'"'; - } - return $attributes ? ' '.implode(' ', $attributes) : ''; - } - - /** - * Converts a word "into-it-s-hyphenated-version" (UTF-8 safe). - * - * A hyphenated word must begin with a letter ([A-Za-z]) and may be - * followed by any number of letters, digits ([0-9]), hyphens ("-"), - * underscores ("_"), colons (":"), and periods ("."). - * - * @param string $word Word to hyphenate - * @param array $language A language code used to convert the word. - * - * @return string The hyphenated word - */ - protected function hyphenize($word, $language = null) - { - // Set locale for transliterating Unicode text to plain ASCII text - $locale = setlocale(LC_CTYPE, 0); - setlocale(LC_CTYPE, 'en_US.UTF8'); - - // Ensure word is UTF-8 encoded - $text = html_entity_decode($word, ENT_COMPAT, 'UTF-8'); - - // Strip tags - $text = strip_tags($text); - - // Perform some language dependent replacements - $lang = $language ? [$language]: $this->language; - $replacements = self::getGrav()['language']->translate('PLUGINS.TOC.PATTERNS', $lang, true); - $text = preg_replace(array_keys($replacements), $replacements, $text); - - // Trim - $text = trim($text, '-'); - - // Transliterate - if (function_exists('iconv')) { - $text = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $text); - } else { - $text = Encoding::toLatin1(Encoding::toUTF8($text)); - } + // Remove unwanted characters and duplicate dashes + $text = preg_replace('~[^-\w]+~', '', $text); - // Lowercase - $text = strtolower($text); + // Trim dashes from the beginning and end of string + $text = trim($text, '.-_ '); - // Remove unwanted characters and duplicate dashes - $text = preg_replace('~[^-\w]+~', '', $text); + // Truncate string + if ($options && $options->get('slug.truncate')) { + $limit = $options->get('slug.length', 32); + $break = $options->get('slug.break', '-'); + $pad = $options->get('slug.pad', '-...'); + $level = $options->get('slug.granularity', 'words'); - // Trim dashes from the beginning and end of string - $text = trim($text, '.-_ '); + $text = $this->truncate($text, $limit, $break, $pad, $level); + } - // Truncate string - if ($this->options->get('slug.truncate')) { - $limit = $this->options->get('slug.length', 32); - $break = $this->options->get('slug.break', '-'); - $pad = $this->options->get('slug.pad', '-...'); + // Restore locale + setlocale(LC_CTYPE, $locale); + $text = trim($text); - $text = $this->truncate($text, $limit, $break, $pad); + // Return hyphenated word or provide default + return $text ?: $l->translate('PLUGINS.TOC.NOT_AVAILABLE', [$lang]); } - // Provide default - if (empty($text)) { - return 'n-a'; - } + /** + * Truncates a string to a maximum length at word or character boundaries. + * + * @param string $string The string which should be truncated + * @param integer $limit The maximum length the string should have + * after truncating + * @param string $break The break delimiter to divide the string + * into pieces of words. + * @param string $pad Added to the end of the truncated string + * @param string $granularity Truncate at "words" or at "characters" + * level + * + * @return string The truncated string + */ + protected function truncate($string, $limit = 32, $break = '-', $pad = '-...', $granularity = 'words') + { + if (mb_strlen($string) > $limit) { + // Truncate string to a maximum length + $truncated = mb_substr($string, 0, $limit); + + // Truncate at words granularity + if ($granularity == 'words') { + $breakpoint = mb_strrpos($truncated, $break); + if ($breakpoint !== false) { + $truncated = mb_substr($string, 0, $breakpoint); + } + } - // Restore locale - setlocale(LC_CTYPE, $locale); - - // Return hyphenated word - return $text; - } - - /** - * Truncates a string to a maximum length at word boundaries. - * - * @param string $string The string which should be truncated. - * @param integer $limit The maximum length the string should have - * after truncating. - * @param string $break The break delimiter to divide the string - * into pieces of words. - * @param string $pad Added to the end of the truncated string. - * - * @return string The truncated string, - */ - protected function truncate($string, $limit = 32, $break = '-', $pad = '-...') - { - $charset = 'UTF-8'; - if (mb_strlen($string, $charset) > $limit) { - if (false !== ($breakpoint = strpos($string, $break, $limit))) { - if ($breakpoint < mb_strlen($string, $charset) - 1) { - $string = mb_substr($string, 0, $breakpoint, $charset); + // Add truncate marker to the end of the string + $string = preg_replace('~(\w)[^\p{L}]?$~', '$1' . $pad, $truncated); } - } else { - // Truncate string to a maximum length - $string = substr($string, 0, $limit); - } - // Add truncate marker to the end of the string - $string = preg_replace('~(\w)[^\p{L}]?$~', "$1$pad", $string); + return $string; } - - return $string; - } } diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..0b80d52 --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "grav-plugin-toc", + "description": "This plugin automagically generates a (minified) Table of Contents based on special markers in the document and adds it into the resulting HTML document.", + "keywords": ["toc", "filter", "formatter", "plugin"], + "homepage": "https://github.com/sommerregen/grav-plugin-toc", + "license": "MIT/GPL", + "authors": [ + { + "name": "Sommerregen", + "email": "sommerregen@benjamin-regler.de", + "homepage": "https://benjamin-regler.de", + "role": "Developer" + } + ], + "support": { + "email": "sommerregen@benjamin-regler.de", + "issues": "https://github.com/sommerregen/grav-plugin-toc/issues", + "irc": "https://gitter.im/sommerregen/chat", + "forum": "http://getgrav.org/forum", + "docs": "https://github.com/sommerregen/grav-plugin-toc/blob/master/README.md" + }, + "require": { + "php": ">=5.6.0", + "neitanod/forceutf8": "~2.0" + }, + "autoload": { + "psr-4": { + "Grav\\Plugin\\Toc\\": "classes/" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..30f98da --- /dev/null +++ b/composer.lock @@ -0,0 +1,55 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "9b23be33b6f3f4c9e8a89b456818e770", + "content-hash": "295614616a213b91919a15e85623378b", + "packages": [ + { + "name": "neitanod/forceutf8", + "version": "v2.0.1", + "source": { + "type": "git", + "url": "https://github.com/neitanod/forceutf8.git", + "reference": "47c883ab2739e7938a8bb0bfd1c29d48c88858de" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/neitanod/forceutf8/zipball/47c883ab2739e7938a8bb0bfd1c29d48c88858de", + "reference": "47c883ab2739e7938a8bb0bfd1c29d48c88858de", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "ForceUTF8\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "authors": [ + { + "name": "Sebastián Grignoli", + "email": "grignoli@gmail.com" + } + ], + "description": "PHP Class Encoding featuring popular Encoding::toUTF8() function --formerly known as forceUTF8()-- that fixes mixed encoded strings.", + "homepage": "https://github.com/neitanod/forceutf8", + "time": "2017-05-22 18:50:57" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.6.0" + }, + "platform-dev": [] +} diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 104fd5b..afc4422 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -31,7 +31,7 @@ As development for `Toc` continues, new versions may become available that add a The simplest way to update this plugin is via the [Grav Package Manager (GPM)](http://learn.getgrav.org/advanced/grav-gpm). You can do this with this by navigating to the root directory of your Grav install using your system's Terminal (also called command line) and typing the following: - bin/gpm update shortcodes + bin/gpm update toc This command will check your Grav install to see if your `Toc` plugin is due for an update. If a newer release is found, you will be asked whether or not you wish to update. To continue, type `y` and hit enter. The plugin will automatically update and clear Grav's cache. diff --git a/languages.yaml b/languages.yaml index f80aaaf..a7fc8b7 100644 --- a/languages.yaml +++ b/languages.yaml @@ -5,7 +5,10 @@ de: GLOBAL_CONFIG: "Globale Einstellungen" DEFAULT_CONFIG: "Standardeinstellungen für External Links" SPECIFIC_CONFIG: "Globale und seitenspezifische Einstellungen" + PLUGIN_STATUS: "Plugin Status" + PLUGIN_ACTIVE: "Aktiv" + PLUGIN_ACTIVE_HELP: "Verwende diese Option zur (De-)Aktivierung des Plugins auf bestimmten Seiten." BUILTIN_CSS: "Verwende mitgeliefertes CSS" TITLE: "Zeige Titel des Inhaltsverzeichnisses" @@ -28,13 +31,20 @@ de: PROCESS: "Aktiviere Toc-Filter auf Seite" SLUG: - SECTION: "Clean URL Erzeugung" + SECTION: "Anker-ID Erzeugung" TRUNCATE: "Überschriften kürzen" - LENGTH: "Clean URL Länge" + LENGTH: "Anker-ID Länge" BREAK: "Trennzeichen" - BREAK_HELP: "Trennzeichen welches zwischen Wörtern in der Clean URL eingesetzt wird" + BREAK_HELP: "Trennzeichen welches zwischen Wörtern in der Anker-ID eingesetzt wird" PAD: "Addendum" - PAD_HELP: "Zusatz, der am Ende der verkürzte Clean URL kennzeichnet" + PAD_HELP: "Zusatz, der am Ende der verkürzten Anker-ID angezeigt wird" + + GRANULARITY: + LABEL: "Granularität" + WORDS: "Wörter" + CHARACTERS: "Zeichen" + + NOT_AVAILABLE: "unbekannt" PATTERNS: # Replace non letter or digits by - @@ -52,7 +62,10 @@ en: GLOBAL_CONFIG: "Global plugin configurations" DEFAULT_CONFIG: "Default values for External Links configuration" SPECIFIC_CONFIG: "Global and page specific configurations" + PLUGIN_STATUS: "Plugin status" + PLUGIN_ACTIVE: "Active" + PLUGIN_ACTIVE_HELP: "Use this option to (de-)activate this plugin on page." BUILTIN_CSS: "Use built in CSS" TITLE: "Show title in the Table of Contents" @@ -83,6 +96,62 @@ en: PAD: "Addendum" PAD_HELP: "Added to the end of the truncated slug" + GRANULARITY: + LABEL: "Granularity" + WORDS: "words" + CHARACTERS: "characters" + + NOT_AVAILABLE: "n-a" + + PATTERNS: + # Replace non letter or digits by - + '~\&~': 'and' + '~[^\p{L}\d]+~u': '-' + '~(\w{4,})y\-s\-~i': '$1ies-' + '~(\w{4,})\-s\-~i': '$1s-' + + # Character replacements + '~([A-Z]+)([A-Z][a-z])~': '\1-\2' + '~([a-z]{2,})([A-Z])~': '\1-\2' + +# French +fr: + PLUGINS: + TOC: + GLOBAL_CONFIG: "Configuration générale du plugin" + DEFAULT_CONFIG: "Configuration des valeurs par défaut pour les Liens Externes" + SPECIFIC_CONFIG: "Configuration générale et spécifique des pages" + PLUGIN_STATUS: "Statut du plugin" + BUILTIN_CSS: "Utiliser les CSS intégrés" + + TITLE: "Afficher le titre dans la table des matières" + ANCHORLINK: "Afficher les liens d'ancrage" + ANCHORLINK_HELP: "Définir sur Vrai pour créer des liens sur les titres vers eux-mêmes." + PERMALINK: "Ajouter des permaliens aux titres" + PERMALINK_HELP: "Définir sur Vrai pour générer des permaliens au début de chaque titre" + PLACEMENT: "Position du lien d'ancrage" + PLACEMENT_LEFT: "Gauche" + PLACEMENT_RIGHT: "Droite" + VISIBLILITY: "Visibilité du lien d'ancrage" + VISIBLILITY_HOVER: "au survol - Visible au survol" + VISIBLILITY_ALWAYS: "toujours - Toujours visible" + ICON: "Icône du lien d'ancrage" + ICON_HELP: "Le lien par défaut ou un caractère spécifique comme : #, ¶, ❡, et §." + CLASS: "Classes supplémentaires" + CLASS_HELP: "Ajoute la classe fournie au lien d'ancrage" + BASELEVEL: "Niveau de base pour les titres" + HEADINGLEVEL: "Niveau maximum de la position à afficher dans TOC/MINITOC" + + PROCESS: "Activer le filtrage Toc sur la page" + SLUG: + SECTION: "Génération de Slug" + TRUNCATE: "Tronquer les en-têtes pour la génération de slug" + LENGTH: "Longueur de la chaîne slug" + BREAK: "Délimiteur de séparation" + BREAK_HELP: "Le délimiteur pour séparer le slug en mots" + PAD: "Avenant" + PAD_HELP: "Ajouté à la fin du slug tronqué" + PATTERNS: # Replace non letter or digits by - '~\&~': 'and' diff --git a/templates/partials/toc.html.twig b/templates/plugins/toc/toc.html.twig similarity index 92% rename from templates/partials/toc.html.twig rename to templates/plugins/toc/toc.html.twig index a2f3348..dd020fb 100644 --- a/templates/partials/toc.html.twig +++ b/templates/plugins/toc/toc.html.twig @@ -38,7 +38,7 @@ {# Show TOC link based on anchorlinks option #} {% if toc.anchorlink %} -
  • {{ entry.text }}
  • +
  • {{ entry.text }}
  • {% else %}
  • {{ entry.text|truncate(32, " ") }}
  • {% endif %} diff --git a/toc.php b/toc.php index d4aaf89..d72ac45 100644 --- a/toc.php +++ b/toc.php @@ -1,6 +1,6 @@ * @author Benjamin Regler - * @copyright 2015, Benjamin Regler + * @copyright 2015-2017, Benjamin Regler * @license MIT * @license GPLv3 */ @@ -23,189 +23,209 @@ use Grav\Common\Plugin; use Grav\Common\Data\Data; use Grav\Common\Page\Page; -use Grav\Plugin\Shortcodes; +use Grav\Plugin\Shortcodes +; use RocketTheme\Toolbox\Event\Event; /** * Toc - * - * This plugin automagically generates a (minified) Table of Contents - * based on special markers in the document and adds it into the - * resulting HTML document. + * @package Grav\Plugin\Toc */ class TocPlugin extends Plugin { - /** --------------------------- - * Private/protected properties - * ---------------------------- - */ - - /** - * Instance of Toc class - * - * @var object - */ - protected $toc; - - /** ------------- - * Public methods - * -------------- - */ - - /** - * Return a list of subscribed events. - * - * @return array The list of events of the plugin of the form - * 'name' => ['method_name', priority]. - */ - public static function getSubscribedEvents() - { - return [ - 'onPluginsInitialized' => ['onPluginsInitialized', 0], - ]; - } - - /** - * Initialize configuration - */ - public function onPluginsInitialized() - { - if ($this->isAdmin()) { - $this->active = false; - return; + /** + * Return a list of subscribed events. + * + * @return array The list of events of the plugin of the form + * 'name' => ['method_name', priority]. + */ + public static function getSubscribedEvents() + { + return [ + 'onPluginsInitialized' => ['onPluginsInitialized', 0], + ]; } - if ($this->config->get('plugins.toc.enabled')) { - $this->enable([ - // Page - 'onPageContentProcessed' => ['onPageContentProcessed', 0], + /** + * Initialize configuration + */ + public function onPluginsInitialized() + { + if ($this->isAdmin()) { + $this->active = false; + return; + } + + $this->enable([ + // Page + 'onPageContentProcessed' => ['onPageContentProcessed', 0], + + // Twig + 'onTwigInitialized' => ['onTwigInitialized', 0], + 'onTwigSiteVariables' => ['onTwigSiteVariables', 0], + 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], + + // Shortcodes + 'onShortcodesInitialized' => ['onShortcodesInitialized', 0] + ]); + } - // Twig - 'onTwigInitialized' => ['onTwigInitialized', 0], - 'onTwigSiteVariables' => ['onTwigSiteVariables', 0], - 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], + /** + * Apply TOC filter to content, when each page has not been + * cached yet. + * + * @param Event $event The event when 'onPageContentProcessed' was + * fired. + */ + public function onPageContentProcessed(Event $event) + { + /** @var Page $page */ + $page = $event['page']; + + $config = $this->mergeConfig($page); + + $active = $config->get('active', $config->get('process')); + if ($active && $config->get('enabled')) { + // Get content, apply TocFilter and save modified page content + $content = $page->getRawContent(); + $page->setRawContent( + $this->tocFilter($content, $config->toArray(), $page) + ); + } + } - // Shortcodes - 'onShortcodesInitialized' => ['onShortcodesInitialized', 0] - ]); + /** + * Initialize Twig configuration and filters. + */ + public function onTwigInitialized() + { + /** @var Twig_Environment $twig */ + $twig = $this->grav['twig']->twig(); + + // Register and expose plugin filters + $filters = ['toc', 'tocify']; + foreach ($filters as $filter) { + $method = [$this, strtolower($filter) . 'Filter']; + $filter = new \Twig_SimpleFilter($filter, $method, ['is_safe' => ['html']]); + $twig->addFilter($filter); + } } - } - - /** - * Apply TOC filter to content, when each page has not been - * cached yet. - * - * @param Event $event The event when 'onPageContentProcessed' was - * fired. - */ - public function onPageContentProcessed(Event $event) - { - /** @var Page $page */ - $page = $event['page']; - - $config = $this->mergeConfig($page); - if ($config->get('enabled') && $config->get('process')) { - // Get content, apply TocFilter and save modified page content - $content = $page->getRawContent(); - $page->setRawContent( - $this->tocFilter($content, $config->toArray(), $page) - ); + + /** + * Add current directory to Twig lookup paths. + */ + public function onTwigTemplatePaths() + { + $this->grav['twig']->twig_paths[] = __DIR__ . '/templates'; } - } - - /** - * Initialize Twig configuration and filters. - */ - public function onTwigInitialized() - { - // Expose tocFilter - $this->grav['twig']->twig()->addFilter( - new \Twig_SimpleFilter('toc', [$this, 'tocFilter'], ['is_safe' => ['html']]) - ); - } - - /** - * Add current directory to Twig lookup paths. - */ - public function onTwigTemplatePaths() - { - $this->grav['twig']->twig_paths[] = __DIR__ . '/templates'; - } - - /** - * Set needed variables to display a table of contents. - */ - public function onTwigSiteVariables() { - if ($this->config->get('plugins.toc.built_in_css')) { - $this->grav['assets']->add('plugin://toc/assets/css/toc.css'); + + /** + * Set needed variables to display a table of contents. + */ + public function onTwigSiteVariables() + { + if ($this->config->get('plugins.toc.built_in_css')) { + $this->grav['assets']->add('plugin://toc/assets/css/toc.css'); + } } - } - - /** - * Filter to automatically create a (minified) table of contents. - * - * @param string $content The content to be filtered - * @param array $options Array of options for the TOC filter - * - * @return string The content with inserted anchor- and - * permalinks in headings and table of contents - * blocks - */ - public function tocFilter($content, $params = []) - { - // Get custom user configuration - $page = func_num_args() > 2 ? func_get_arg(2) : $this->grav['page']; - $config = $this->mergeConfig($page, true, $params); - - // Render Toc - return $this->init()->render($content, $config, $page); - } - - /** - * Register {{% toc %}}, {{% minitoc %}} and {{% tocify %}} shortcodes. - * - * @param Event $event An event object. - */ - public function onShortcodesInitialized(Event $event) - { - $toc = $this->init(); - $function = function($event) { - // Update header to bypass evaluation - if (isset($event['page']->header()->toc->enabled)) { - $event['page']->header()->toc->enabled = true; - } - - return '['.strtoupper($event['tag']).']'; - }; - - $shortcodes = [ - new Shortcodes\InlineShortcode('toc', $function), - new Shortcodes\InlineShortcode('minitoc', $function), - // new Shortcodes\BlockShortcode('tocify', [$toc, 'tocifyShortcode']) - ]; - - // Register {{% toc %}}, {{% minitoc %}} and {{% tocify %}} shortcode - $event['shortcodes']->register($shortcodes); - } - - /** ------------------------------- - * Private/protected helper methods - * -------------------------------- - */ - - /** - * Initialize plugin and all dependencies. - * - * @return \Grav\Plugin\ExternalLinks Returns ExternalLinks instance. - */ - protected function init() - { - if (!$this->toc) { - // Initialize Toc class - require_once(__DIR__ . '/classes/Toc.php'); - $this->toc = new Toc(); + + /** + * Filter to automatically create a (minified) table of contents. + * + * @param string $content The content to be filtered + * @param array $params Array of options for the TOC filter + * + * @return string The content with inserted anchor- and + * permalinks in headings and table of contents + * blocks + */ + public function tocFilter($content, $params = []) + { + // Resolve page and page content + if ($content instanceof Page) { + $page = $content; + $content = $page->content(); + } else { + $page = func_num_args() > 2 ? func_get_arg(2) : $this->grav['page']; + } + + // Get custom user configuration + $config = $this->mergeConfig($page, true, $params); + + // Render Toc + return $this->init()->render($content, $config, $page); } - return $this->toc; - } + /** + * Filter to return a (minified) table of contents of the text. + * + * @param string $content The content to be filtered + * @param array $params Array of options for the tocify filter + * + * @return array An array with a list of elements + */ + public function tocifyFilter($content, $params = []) + { + // Resolve page and page content + if ($content instanceof Page) { + $page = $content; + $content = $page->content(); + } else { + $page = func_num_args() > 2 ? func_get_arg(2) : $this->grav['page']; + } + + // Get custom user configuration + $config = $this->mergeConfig($page, true, $params); + $config->set('language', $page->language()); + + // Just generate a table of contents for the current document + return $this->init()->createToc($content, $config); + } + + /** + * Register {{% toc %}}, {{% minitoc %}} and {{% tocify %}} shortcodes. + * + * @param Event $event An event object + */ + public function onShortcodesInitialized(Event $event) + { + $toc = $this->init(); + $function = function($event) { + // Update header to bypass evaluation + if (isset($event['page']->header()->toc->enabled)) { + $event['page']->header()->toc->enabled = true; + } + + return '[' . strtoupper($event['tag']) . ']'; + }; + + $shortcodes = [ + new Shortcodes\InlineShortcode('toc', $function), + new Shortcodes\InlineShortcode('minitoc', $function), + // new Shortcodes\BlockShortcode('tocify', [$toc, 'tocifyShortcode']) + ]; + + // Register {{% toc %}}, {{% minitoc %}} and {{% tocify %}} shortcode + $event['shortcodes']->register($shortcodes); + } + + /** + * Initialize plugin and all dependencies. + * + * @return \Grav\Plugin\Toc + */ + protected function init() + { + static $instance = null; + + // Initialize Toc class + if (!$instance) { + require_once(__DIR__ . '/classes/Toc.php'); + require_once(__DIR__ . '/vendor/neitanod/forceutf8/src/ForceUTF8/Encoding.php'); + + $instance = new Toc(); + } + + return $instance; + } } diff --git a/toc.yaml b/toc.yaml index 6022009..c05bcd8 100644 --- a/toc.yaml +++ b/toc.yaml @@ -1,18 +1,19 @@ # Global plugin configurations enabled: true # Set to false to disable this plugin completely +active: true # Option to (de-)activate this plugin on a page built_in_css: true # Use built-in CSS of the plugin # Global and page specific configurations -title: true # Title to insert in the Table of Contents +title: true # Title to insert in the table of contents anchorlink: true # Set to true to cause all headers to link to themselves permalink: true # Set to true to generate permanent links at the beginning of each header placement: "left" # Either "left" or "right" visible: "hover" # Active on "hover" or "always" visible -icon: # Default link or a specific character like: #, ¶, ❡, and §. -class: # Adds the provided class to the anchor html +icon: "#" # Default link or a specific character like: #, ¶, ❡, and § +class: [] # Adds the provided classes to the anchor HTML baselevel: 1 # Base level for headings headinglevel: 6 # Maximum heading level to show in TOC @@ -21,6 +22,5 @@ slug: # Slug generation truncate: true # Truncate headings for slug generation length: 32 # Slug string length break: "-" # The break delimiter to divide the slug into pieces of words. - pad: "-..." # Added to the end of the truncated slug - -process: true # Process table of contents + pad: "..." # Added to the end of the truncated slug + level: "words" # Kind of slug generation (based on "words" or "character") diff --git a/vendor/neitanod/forceutf8/README.md b/vendor/neitanod/forceutf8/README.md new file mode 100644 index 0000000..765c637 --- /dev/null +++ b/vendor/neitanod/forceutf8/README.md @@ -0,0 +1,61 @@ +forceutf8 +========= + +PHP Class Encoding featuring popular \ForceUTF8\Encoding::toUTF8() function --formerly known as forceUTF8()-- that fixes mixed encoded strings. + +Description +=========== + +If you apply the PHP function utf8_encode() to an already-UTF8 string it will return a garbled UTF8 string. + +This class addresses this issue and provides a handy static function called \ForceUTF8\Encoding::toUTF8(). + +You don't need to know what the encoding of your strings is. It can be Latin1 (iso 8859-1), Windows-1252 or UTF8, or the string can have a mix of them. \ForceUTF8\Encoding::toUTF8() will convert everything to UTF8. + +Sometimes you have to deal with services that are unreliable in terms of encoding, possibly mixing UTF8 and Latin1 in the same string. + +Update: + +I've included another function, \ForceUTF8\Encoding::fixUTF8(), which will fix the double (or multiple) encoded UTF8 string that looks garbled. + +Usage: +====== + + use \ForceUTF8\Encoding; + + $utf8_string = Encoding::toUTF8($utf8_or_latin1_or_mixed_string); + + $latin1_string = Encoding::toLatin1($utf8_or_latin1_or_mixed_string); + +also: + + $utf8_string = Encoding::fixUTF8($garbled_utf8_string); + +Examples: + + use \ForceUTF8\Encoding; + + echo Encoding::fixUTF8("Fédération Camerounaise de Football\n"); + echo Encoding::fixUTF8("Fédération Camerounaise de Football\n"); + echo Encoding::fixUTF8("Fédération Camerounaise de Football\n"); + echo Encoding::fixUTF8("Fédération Camerounaise de Football\n"); + +will output: + + Fédération Camerounaise de Football + Fédération Camerounaise de Football + Fédération Camerounaise de Football + Fédération Camerounaise de Football + +Install via composer: +===================== +Edit your composer.json file to include the following: + +```json +{ + "require": { + "neitanod/forceutf8": "~2.0" + } +} +``` + diff --git a/vendor/neitanod/forceutf8/composer.json b/vendor/neitanod/forceutf8/composer.json new file mode 100644 index 0000000..aff71af --- /dev/null +++ b/vendor/neitanod/forceutf8/composer.json @@ -0,0 +1,20 @@ +{ + "name": "neitanod/forceutf8", + "homepage": "https://github.com/neitanod/forceutf8", + "type": "library", + "description": "PHP Class Encoding featuring popular Encoding::toUTF8() function --formerly known as forceUTF8()-- that fixes mixed encoded strings.", + "require": { + "php": ">=5.3.0" + }, + "authors": [ + { + "name": "Sebastián Grignoli", + "email": "grignoli@gmail.com" + } + ], + "autoload": { + "psr-0": { + "ForceUTF8\\": "src/" + } + } +} diff --git a/vendor/neitanod/forceutf8/src/ForceUTF8/Encoding.php b/vendor/neitanod/forceutf8/src/ForceUTF8/Encoding.php new file mode 100644 index 0000000..6530576 --- /dev/null +++ b/vendor/neitanod/forceutf8/src/ForceUTF8/Encoding.php @@ -0,0 +1,347 @@ + + * @package Encoding + * @version 2.0 + * @link https://github.com/neitanod/forceutf8 + * @example https://github.com/neitanod/forceutf8 + * @license Revised BSD + */ + +namespace ForceUTF8; + +class Encoding { + + const ICONV_TRANSLIT = "TRANSLIT"; + const ICONV_IGNORE = "IGNORE"; + const WITHOUT_ICONV = ""; + + protected static $win1252ToUtf8 = array( + 128 => "\xe2\x82\xac", + + 130 => "\xe2\x80\x9a", + 131 => "\xc6\x92", + 132 => "\xe2\x80\x9e", + 133 => "\xe2\x80\xa6", + 134 => "\xe2\x80\xa0", + 135 => "\xe2\x80\xa1", + 136 => "\xcb\x86", + 137 => "\xe2\x80\xb0", + 138 => "\xc5\xa0", + 139 => "\xe2\x80\xb9", + 140 => "\xc5\x92", + + 142 => "\xc5\xbd", + + + 145 => "\xe2\x80\x98", + 146 => "\xe2\x80\x99", + 147 => "\xe2\x80\x9c", + 148 => "\xe2\x80\x9d", + 149 => "\xe2\x80\xa2", + 150 => "\xe2\x80\x93", + 151 => "\xe2\x80\x94", + 152 => "\xcb\x9c", + 153 => "\xe2\x84\xa2", + 154 => "\xc5\xa1", + 155 => "\xe2\x80\xba", + 156 => "\xc5\x93", + + 158 => "\xc5\xbe", + 159 => "\xc5\xb8" + ); + + protected static $brokenUtf8ToUtf8 = array( + "\xc2\x80" => "\xe2\x82\xac", + + "\xc2\x82" => "\xe2\x80\x9a", + "\xc2\x83" => "\xc6\x92", + "\xc2\x84" => "\xe2\x80\x9e", + "\xc2\x85" => "\xe2\x80\xa6", + "\xc2\x86" => "\xe2\x80\xa0", + "\xc2\x87" => "\xe2\x80\xa1", + "\xc2\x88" => "\xcb\x86", + "\xc2\x89" => "\xe2\x80\xb0", + "\xc2\x8a" => "\xc5\xa0", + "\xc2\x8b" => "\xe2\x80\xb9", + "\xc2\x8c" => "\xc5\x92", + + "\xc2\x8e" => "\xc5\xbd", + + + "\xc2\x91" => "\xe2\x80\x98", + "\xc2\x92" => "\xe2\x80\x99", + "\xc2\x93" => "\xe2\x80\x9c", + "\xc2\x94" => "\xe2\x80\x9d", + "\xc2\x95" => "\xe2\x80\xa2", + "\xc2\x96" => "\xe2\x80\x93", + "\xc2\x97" => "\xe2\x80\x94", + "\xc2\x98" => "\xcb\x9c", + "\xc2\x99" => "\xe2\x84\xa2", + "\xc2\x9a" => "\xc5\xa1", + "\xc2\x9b" => "\xe2\x80\xba", + "\xc2\x9c" => "\xc5\x93", + + "\xc2\x9e" => "\xc5\xbe", + "\xc2\x9f" => "\xc5\xb8" + ); + + protected static $utf8ToWin1252 = array( + "\xe2\x82\xac" => "\x80", + + "\xe2\x80\x9a" => "\x82", + "\xc6\x92" => "\x83", + "\xe2\x80\x9e" => "\x84", + "\xe2\x80\xa6" => "\x85", + "\xe2\x80\xa0" => "\x86", + "\xe2\x80\xa1" => "\x87", + "\xcb\x86" => "\x88", + "\xe2\x80\xb0" => "\x89", + "\xc5\xa0" => "\x8a", + "\xe2\x80\xb9" => "\x8b", + "\xc5\x92" => "\x8c", + + "\xc5\xbd" => "\x8e", + + + "\xe2\x80\x98" => "\x91", + "\xe2\x80\x99" => "\x92", + "\xe2\x80\x9c" => "\x93", + "\xe2\x80\x9d" => "\x94", + "\xe2\x80\xa2" => "\x95", + "\xe2\x80\x93" => "\x96", + "\xe2\x80\x94" => "\x97", + "\xcb\x9c" => "\x98", + "\xe2\x84\xa2" => "\x99", + "\xc5\xa1" => "\x9a", + "\xe2\x80\xba" => "\x9b", + "\xc5\x93" => "\x9c", + + "\xc5\xbe" => "\x9e", + "\xc5\xb8" => "\x9f" + ); + + static function toUTF8($text){ + /** + * Function \ForceUTF8\Encoding::toUTF8 + * + * This function leaves UTF8 characters alone, while converting almost all non-UTF8 to UTF8. + * + * It assumes that the encoding of the original string is either Windows-1252 or ISO 8859-1. + * + * It may fail to convert characters to UTF-8 if they fall into one of these scenarios: + * + * 1) when any of these characters: ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖ×ØÙÚÛÜÝÞß + * are followed by any of these: ("group B") + * ¡¢£¤¥¦§¨©ª«¬­®¯°±²³´µ¶•¸¹º»¼½¾¿ + * For example: %ABREPRESENT%C9%BB. «REPRESENTÉ» + * The "«" (%AB) character will be converted, but the "É" followed by "»" (%C9%BB) + * is also a valid unicode character, and will be left unchanged. + * + * 2) when any of these: àáâãäåæçèéêëìíîï are followed by TWO chars from group B, + * 3) when any of these: ðñòó are followed by THREE chars from group B. + * + * @name toUTF8 + * @param string $text Any string. + * @return string The same string, UTF8 encoded + * + */ + + if(is_array($text)) + { + foreach($text as $k => $v) + { + $text[$k] = self::toUTF8($v); + } + return $text; + } + + if(!is_string($text)) { + return $text; + } + + $max = self::strlen($text); + + $buf = ""; + for($i = 0; $i < $max; $i++){ + $c1 = $text{$i}; + if($c1>="\xc0"){ //Should be converted to UTF8, if it's not UTF8 already + $c2 = $i+1 >= $max? "\x00" : $text{$i+1}; + $c3 = $i+2 >= $max? "\x00" : $text{$i+2}; + $c4 = $i+3 >= $max? "\x00" : $text{$i+3}; + if($c1 >= "\xc0" & $c1 <= "\xdf"){ //looks like 2 bytes UTF8 + if($c2 >= "\x80" && $c2 <= "\xbf"){ //yeah, almost sure it's UTF8 already + $buf .= $c1 . $c2; + $i++; + } else { //not valid UTF8. Convert it. + $cc1 = (chr(ord($c1) / 64) | "\xc0"); + $cc2 = ($c1 & "\x3f") | "\x80"; + $buf .= $cc1 . $cc2; + } + } elseif($c1 >= "\xe0" & $c1 <= "\xef"){ //looks like 3 bytes UTF8 + if($c2 >= "\x80" && $c2 <= "\xbf" && $c3 >= "\x80" && $c3 <= "\xbf"){ //yeah, almost sure it's UTF8 already + $buf .= $c1 . $c2 . $c3; + $i = $i + 2; + } else { //not valid UTF8. Convert it. + $cc1 = (chr(ord($c1) / 64) | "\xc0"); + $cc2 = ($c1 & "\x3f") | "\x80"; + $buf .= $cc1 . $cc2; + } + } elseif($c1 >= "\xf0" & $c1 <= "\xf7"){ //looks like 4 bytes UTF8 + if($c2 >= "\x80" && $c2 <= "\xbf" && $c3 >= "\x80" && $c3 <= "\xbf" && $c4 >= "\x80" && $c4 <= "\xbf"){ //yeah, almost sure it's UTF8 already + $buf .= $c1 . $c2 . $c3 . $c4; + $i = $i + 3; + } else { //not valid UTF8. Convert it. + $cc1 = (chr(ord($c1) / 64) | "\xc0"); + $cc2 = ($c1 & "\x3f") | "\x80"; + $buf .= $cc1 . $cc2; + } + } else { //doesn't look like UTF8, but should be converted + $cc1 = (chr(ord($c1) / 64) | "\xc0"); + $cc2 = (($c1 & "\x3f") | "\x80"); + $buf .= $cc1 . $cc2; + } + } elseif(($c1 & "\xc0") == "\x80"){ // needs conversion + if(isset(self::$win1252ToUtf8[ord($c1)])) { //found in Windows-1252 special cases + $buf .= self::$win1252ToUtf8[ord($c1)]; + } else { + $cc1 = (chr(ord($c1) / 64) | "\xc0"); + $cc2 = (($c1 & "\x3f") | "\x80"); + $buf .= $cc1 . $cc2; + } + } else { // it doesn't need conversion + $buf .= $c1; + } + } + return $buf; + } + + static function toWin1252($text, $option = self::WITHOUT_ICONV) { + if(is_array($text)) { + foreach($text as $k => $v) { + $text[$k] = self::toWin1252($v, $option); + } + return $text; + } elseif(is_string($text)) { + return static::utf8_decode($text, $option); + } else { + return $text; + } + } + + static function toISO8859($text) { + return self::toWin1252($text); + } + + static function toLatin1($text) { + return self::toWin1252($text); + } + + static function fixUTF8($text, $option = self::WITHOUT_ICONV){ + if(is_array($text)) { + foreach($text as $k => $v) { + $text[$k] = self::fixUTF8($v, $option); + } + return $text; + } + + $last = ""; + while($last <> $text){ + $last = $text; + $text = self::toUTF8(static::utf8_decode($text, $option)); + } + $text = self::toUTF8(static::utf8_decode($text, $option)); + return $text; + } + + static function UTF8FixWin1252Chars($text){ + // If you received an UTF-8 string that was converted from Windows-1252 as it was ISO8859-1 + // (ignoring Windows-1252 chars from 80 to 9F) use this function to fix it. + // See: http://en.wikipedia.org/wiki/Windows-1252 + + return str_replace(array_keys(self::$brokenUtf8ToUtf8), array_values(self::$brokenUtf8ToUtf8), $text); + } + + static function removeBOM($str=""){ + if(substr($str, 0,3) == pack("CCC",0xef,0xbb,0xbf)) { + $str=substr($str, 3); + } + return $str; + } + + protected static function strlen($text){ + return (function_exists('mb_strlen') && ((int) ini_get('mbstring.func_overload')) & 2) ? + mb_strlen($text,'8bit') : strlen($text); + } + + public static function normalizeEncoding($encodingLabel) + { + $encoding = strtoupper($encodingLabel); + $encoding = preg_replace('/[^a-zA-Z0-9\s]/', '', $encoding); + $equivalences = array( + 'ISO88591' => 'ISO-8859-1', + 'ISO8859' => 'ISO-8859-1', + 'ISO' => 'ISO-8859-1', + 'LATIN1' => 'ISO-8859-1', + 'LATIN' => 'ISO-8859-1', + 'UTF8' => 'UTF-8', + 'UTF' => 'UTF-8', + 'WIN1252' => 'ISO-8859-1', + 'WINDOWS1252' => 'ISO-8859-1' + ); + + if(empty($equivalences[$encoding])){ + return 'UTF-8'; + } + + return $equivalences[$encoding]; + } + + public static function encode($encodingLabel, $text) + { + $encodingLabel = self::normalizeEncoding($encodingLabel); + if($encodingLabel == 'ISO-8859-1') return self::toLatin1($text); + return self::toUTF8($text); + } + + protected static function utf8_decode($text, $option) + { + if ($option == self::WITHOUT_ICONV || !function_exists('iconv')) { + $o = utf8_decode( + str_replace(array_keys(self::$utf8ToWin1252), array_values(self::$utf8ToWin1252), self::toUTF8($text)) + ); + } else { + $o = iconv("UTF-8", "Windows-1252" . ($option == self::ICONV_TRANSLIT ? '//TRANSLIT' : ($option == self::ICONV_IGNORE ? '//IGNORE' : '')), $text); + } + return $o; + } +} diff --git a/vendor/neitanod/forceutf8/test/ForceUTF8Test.php b/vendor/neitanod/forceutf8/test/ForceUTF8Test.php new file mode 100644 index 0000000..02ec687 --- /dev/null +++ b/vendor/neitanod/forceutf8/test/ForceUTF8Test.php @@ -0,0 +1,101 @@ + FAILED\n"; + static::$failed++; + } + + private static function passed($test_name){ + static::character("."); + static::$passed++; + } + + private static function character($char){ + echo $char; + static::$last_echoed = 'char'; + } + + private static function line($msg){ + if(static::$last_echoed == 'char') echo "\n"; + echo $msg."\n"; + static::$last_echoed = 'line'; + } + } + diff --git a/vendor/neitanod/forceutf8/test/data/russian.txt b/vendor/neitanod/forceutf8/test/data/russian.txt new file mode 100644 index 0000000..1c618ad --- /dev/null +++ b/vendor/neitanod/forceutf8/test/data/russian.txt @@ -0,0 +1 @@ +hello žš, привет diff --git a/vendor/neitanod/forceutf8/test/data/test1.txt b/vendor/neitanod/forceutf8/test/data/test1.txt new file mode 100644 index 0000000..771829e --- /dev/null +++ b/vendor/neitanod/forceutf8/test/data/test1.txt @@ -0,0 +1 @@ +Hírek diff --git a/vendor/neitanod/forceutf8/test/data/test1Latin.txt b/vendor/neitanod/forceutf8/test/data/test1Latin.txt new file mode 100644 index 0000000..0aa69d6 --- /dev/null +++ b/vendor/neitanod/forceutf8/test/data/test1Latin.txt @@ -0,0 +1 @@ +Hrek