From 7ea43a97fcee97539fe35f391ffa7cf0e9743355 Mon Sep 17 00:00:00 2001 From: Casey McLaughlin Date: Tue, 30 Dec 2014 13:53:32 -0500 Subject: [PATCH] Initial Commit --- .gitignore | 6 + .idea/.name | 1 + .idea/encodings.xml | 4 + .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/scopes/scope_settings.xml | 5 + .idea/toc.iml | 8 + .idea/vcs.xml | 6 + CHANGELOG.md | 0 LICENSE | 20 + README.md | 117 ++++ composer.json | 32 ++ composer.lock | 971 ++++++++++++++++++++++++++++++++ phpunit.xml.dist | 20 + src/HeaderTagInterpreter.php | 32 ++ src/Sluggifier.php | 67 +++ src/TocGenerator.php | 107 ++++ src/TocMarkupFixer.php | 81 +++ src/TocTwigExtension.php | 85 +++ tests/TocGeneratorTest.php | 90 +++ tests/TocMarkupFixerTest.php | 60 ++ tests/bootstrap.php | 27 + 22 files changed, 1751 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.name create mode 100644 .idea/encodings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/scopes/scope_settings.xml create mode 100644 .idea/toc.iml create mode 100644 .idea/vcs.xml create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 phpunit.xml.dist create mode 100644 src/HeaderTagInterpreter.php create mode 100644 src/Sluggifier.php create mode 100644 src/TocGenerator.php create mode 100644 src/TocMarkupFixer.php create mode 100644 src/TocTwigExtension.php create mode 100644 tests/TocGeneratorTest.php create mode 100644 tests/TocMarkupFixerTest.php create mode 100644 tests/bootstrap.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..249d009 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# files +vendor/ +phpunit.xml + +# IDE stuff +.idea/workspace.xml \ No newline at end of file diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..5a4bd14 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +toc \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..d821048 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8662aa9 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ccd14d8 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/scopes/scope_settings.xml b/.idea/scopes/scope_settings.xml new file mode 100644 index 0000000..922003b --- /dev/null +++ b/.idea/scopes/scope_settings.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/.idea/toc.iml b/.idea/toc.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/toc.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..6564d52 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..abbf69a --- /dev/null +++ b/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Casey McLaughlin + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..cf672fc --- /dev/null +++ b/README.md @@ -0,0 +1,117 @@ +PHP TOC Generator +================= + +Geneates Table of Contents from H1...H6 Tags in HTML Content +------------------------------------------------------------ + +[![Build Status](https://travis-ci.org/caseyamcl/toc)](https://travis-ci.org/caseyamcl/toc.png) + +This library provides a simple, framework-agnostic class to build +a Table-of-Contents from HTML markup. It does so by parsing H1...H6 +tags. It can also automatically add appropriate "id" anchor links to content. + +Features: +* Generates arrays or HTML <li*gt; tags +* Adds anchor IDs to content +* Specify which *H1*...*H6* heading tags to use at runtime +* Includes Twig Extension for generating TOC lists and compatible markup directly from templates +* PSR-0 thru PSR-2 Compliant +* Composer-compatible +* Unit-tested + +In the spirit of KISS philosophy, this library assumes a few things: + +1. The hierarchy of your content is defined solely by the header (*H1*...*H6*) tags. All other tags + are ignored. +2. The link titles in the Table of Contents should match either the `title` attribute of the header tag, + or if there is no `title`, the plaintext body of the header tag. + +Installation Options +-------------------- +Install via [Composer](http://getcomposer.org/) by including the following in your `composer.json` file: + + { + "require": { + "caseyamcl/toc": "~1.0", + } + } + +Or, drop the `src` folder into your application and use a PSR-0 autoloader to include the files. + + +Usage +----- +This library does two things with any HTML content passed in: + +1. Adds `id` tags to any H1..H6 tags (or header-levels specified at runtime) that do + not already have any. This is to enable anchor links in the content. +2. Generates HTML (or an associative array) of anchor links that can be rendered in your + template. + +Basic Example: + +```php +$myHtmlContent = <<This is a header tag with no anchor id +

Lorum ipsum doler sit amet

+

This is a header tag with an anchor id

+

Stuff here

+

This is a header tag with an anchor id

+END; + +$tocContent = new \TOC\TocContent($myHtmlContent); + +// Generate HTML list of links +echo ""; + +``` + +Twig Integration +---------------- +This library includes a [Twig](http://twig.sensiolabs.org) extension that enables you to load +TOC lists and compatible markup from your Twig templates. + +Specifically, the extension adds three Twig functions: + +```twig +{# Generates HTML markup for given htmlContent #} +{# The second two parameters are optional (defaults are h1, h3) #} + + +{# Generates an array of anchor links for given htmlContent #} +{# The second two parameters are optional (defaults are h1, h3) #} + + +{# Adds anchor links (id tags) for given htmlContent #} +{# The second two parameters are optional (defaults are h1, h3) #} +
+ {{ toc_content(htmlContent, 'h1', 'h3') }} +
+``` + +You may have content in hard-coded in your Twig Template that you want to TOC-ize. An +easy way to do this is to make sure the content is surrounded by `{% block %}...{% endblock %}` +tags, and then just pass in that content to the *toc* functions> + +For example: + +```twig +{% extends 'base.html.twig' %} +{% block page_content %} +
+ {{ toc(block('my_writeup'), 'h1', 'h2') }} +
+ +
+ {{ toc_content(block('my_writeup'), 'h1', 'h2') }} +
+{% endblock %} + +{% block my_writeup %} +

+{% endblock %} +``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..25c1dac --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "caseyamcl/toc", + "type": "library", + "description": "Simple Table-of-Contents Generator for PHP. Generates TOCs based off H1...H6 tags", + "keywords": ["table of contents", "toc"], + "homepage": "http://github.com/caseyamcl/toc", + "license": "MIT", + "authors": + [ + { + "name": "Casey McLaughlin", + "email": "caseyamcl@gmail.com", + "homepage": "http://caseymclaughlin.com", + "role": "Developer" + } + + ], + "autoload": { + "psr-4": { + "TOC\\": ["src/", "tests/"] + } + }, + "require": { + "php": ">=5.4", + "sunra/php-simple-html-dom-parser": "~1.5", + "cocur/slugify": "~1.0" + }, + "require-dev": { + "twig/twig": "~1.13", + "phpunit/phpunit": "~4.0" + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..8e0c7f2 --- /dev/null +++ b/composer.lock @@ -0,0 +1,971 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "93020f0823d28448cae16c110561fe2d", + "packages": [ + { + "name": "cocur/slugify", + "version": "v1.0", + "source": { + "type": "git", + "url": "https://github.com/cocur/slugify.git", + "reference": "2c929c56997e663747be9bb635e58db918a0134f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cocur/slugify/zipball/2c929c56997e663747be9bb635e58db918a0134f", + "reference": "2c929c56997e663747be9bb635e58db918a0134f", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "codeclimate/php-test-reporter": "dev-master", + "laravel/framework": "~4.1", + "mockery/mockery": "~0.9", + "phpunit/phpunit": "~3.7", + "sami/sami": "~1.3", + "satooshi/php-coveralls": "0.6.*", + "silex/silex": "~1.2", + "symfony/dependency-injection": "~2.4", + "symfony/http-kernel": "~2.4", + "twig/twig": "~1", + "zendframework/zend-modulemanager": "~2.2", + "zendframework/zend-servicemanager": "~2.2", + "zendframework/zend-view": "~2.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cocur\\Slugify\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ivo Bathke", + "email": "ivo.bathke@gmail.com" + }, + { + "name": "Florian Eckerstorfer", + "email": "florian@eckerstorfer.co", + "homepage": "https://florian.ec" + } + ], + "description": "Converts a string into a slug.", + "keywords": [ + "slug", + "slugify" + ], + "time": "2014-11-26 22:45:14" + }, + { + "name": "sunra/php-simple-html-dom-parser", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/sunra/php-simple-html-dom-parser.git", + "reference": "a0b80ace086c7e09085669205e1b3c2c9c7a453c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sunra/php-simple-html-dom-parser/zipball/a0b80ace086c7e09085669205e1b3c2c9c7a453c", + "reference": "a0b80ace086c7e09085669205e1b3c2c9c7a453c", + "shasum": "" + }, + "require": { + "php": ">=5.3.2" + }, + "type": "library", + "autoload": { + "psr-0": { + "Sunra\\PhpSimple\\HtmlDomParser": "Src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sunra", + "email": "sunra@yandex.ru", + "homepage": "http://github.com/sunra" + } + ], + "description": "Composer adaptation of: A HTML DOM parser written in PHP5+ let you manipulate HTML in a very easy way! Require PHP 5+. Supports invalid HTML. Find tags on an HTML page with selectors just like jQuery. Extract contents from HTML in a single line.", + "homepage": "https://github.com/sunra/php-simple-html-dom-parser", + "keywords": [ + "dom", + "html", + "parser" + ], + "time": "2013-05-04 14:32:03" + } + ], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.0.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "f976e5de371104877ebc89bd8fecb0019ed9c119" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f976e5de371104877ebc89bd8fecb0019ed9c119", + "reference": "f976e5de371104877ebc89bd8fecb0019ed9c119", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "2.0.*@ALPHA" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Instantiator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2014-10-13 12:58:55" + }, + { + "name": "phpunit/php-code-coverage", + "version": "2.0.14", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "ca158276c1200cc27f5409a5e338486bc0b4fc94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca158276c1200cc27f5409a5e338486bc0b4fc94", + "reference": "ca158276c1200cc27f5409a5e338486bc0b4fc94", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "~1.3", + "sebastian/environment": "~1.0", + "sebastian/version": "~1.0" + }, + "require-dev": { + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "~4.1" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.2.1", + "ext-xmlwriter": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2014-12-26 13:28:33" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.3.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/acd690379117b042d1c8af1fafd61bde001bf6bb", + "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "File/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2013-10-10 15:34:57" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", + "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "Text/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2014-01-30 17:20:04" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/19689d4354b295ee3d8c54b4f42c3efb69cbc17c", + "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "PHP/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2013-08-02 07:42:54" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "f8d5d08c56de5cfd592b3340424a81733259a876" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/f8d5d08c56de5cfd592b3340424a81733259a876", + "reference": "f8d5d08c56de5cfd592b3340424a81733259a876", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2014-08-31 06:12:13" + }, + { + "name": "phpunit/phpunit", + "version": "4.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "6a5e49a86ce5e33b8d0657abe145057fc513543a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/6a5e49a86ce5e33b8d0657abe145057fc513543a", + "reference": "6a5e49a86ce5e33b8d0657abe145057fc513543a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.3.3", + "phpunit/php-code-coverage": "~2.0", + "phpunit/php-file-iterator": "~1.3.2", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "~1.0.2", + "phpunit/phpunit-mock-objects": "~2.3", + "sebastian/comparator": "~1.0", + "sebastian/diff": "~1.1", + "sebastian/environment": "~1.1", + "sebastian/exporter": "~1.0", + "sebastian/global-state": "~1.0", + "sebastian/version": "~1.0", + "symfony/yaml": "~2.0" + }, + "suggest": { + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2014-12-28 07:57:05" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "c63d2367247365f688544f0d500af90a11a44c65" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/c63d2367247365f688544f0d500af90a11a44c65", + "reference": "c63d2367247365f688544f0d500af90a11a44c65", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "~1.0,>=1.0.1", + "php": ">=5.3.3", + "phpunit/php-text-template": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.3" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2014-10-03 05:12:11" + }, + { + "name": "sebastian/comparator", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "c484a80f97573ab934e37826dba0135a3301b26a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c484a80f97573ab934e37826dba0135a3301b26a", + "reference": "c484a80f97573ab934e37826dba0135a3301b26a", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.1", + "sebastian/exporter": "~1.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2014-11-16 21:32:38" + }, + { + "name": "sebastian/diff", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "5843509fed39dee4b356a306401e9dd1a931fec7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/5843509fed39dee4b356a306401e9dd1a931fec7", + "reference": "5843509fed39dee4b356a306401e9dd1a931fec7", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "http://www.github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2014-08-15 10:29:00" + }, + { + "name": "sebastian/environment", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "6e6c71d918088c251b181ba8b3088af4ac336dd7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6e6c71d918088c251b181ba8b3088af4ac336dd7", + "reference": "6e6c71d918088c251b181ba8b3088af4ac336dd7", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2014-10-25 08:00:45" + }, + { + "name": "sebastian/exporter", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "c7d59948d6e82818e1bdff7cadb6c34710eb7dc0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c7d59948d6e82818e1bdff7cadb6c34710eb7dc0", + "reference": "c7d59948d6e82818e1bdff7cadb6c34710eb7dc0", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2014-09-10 00:51:36" + }, + { + "name": "sebastian/global-state", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/c7428acdb62ece0a45e6306f1ae85e1c05b09c01", + "reference": "c7428acdb62ece0a45e6306f1ae85e1c05b09c01", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2014-10-06 09:23:50" + }, + { + "name": "sebastian/version", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "b6e1f0cf6b9e1ec409a0d3e2f2a5fb0998e36b43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/b6e1f0cf6b9e1ec409a0d3e2f2a5fb0998e36b43", + "reference": "b6e1f0cf6b9e1ec409a0d3e2f2a5fb0998e36b43", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2014-03-07 15:35:33" + }, + { + "name": "symfony/yaml", + "version": "v2.6.1", + "target-dir": "Symfony/Component/Yaml", + "source": { + "type": "git", + "url": "https://github.com/symfony/Yaml.git", + "reference": "3346fc090a3eb6b53d408db2903b241af51dcb20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/3346fc090a3eb6b53d408db2903b241af51dcb20", + "reference": "3346fc090a3eb6b53d408db2903b241af51dcb20", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-0": { + "Symfony\\Component\\Yaml\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "http://symfony.com/contributors" + }, + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Symfony Yaml Component", + "homepage": "http://symfony.com", + "time": "2014-12-02 20:19:20" + }, + { + "name": "twig/twig", + "version": "v1.16.3", + "source": { + "type": "git", + "url": "https://github.com/twigphp/Twig.git", + "reference": "6dc11a1e8ecfc30e2c68aaeb218148409d8e68af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/6dc11a1e8ecfc30e2c68aaeb218148409d8e68af", + "reference": "6dc11a1e8ecfc30e2c68aaeb218148409d8e68af", + "shasum": "" + }, + "require": { + "php": ">=5.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.16-dev" + } + }, + "autoload": { + "psr-0": { + "Twig_": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com", + "role": "Project Founder" + }, + { + "name": "Twig Team", + "homepage": "http://twig.sensiolabs.org/contributors", + "role": "Contributors" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "http://twig.sensiolabs.org", + "keywords": [ + "templating" + ], + "time": "2014-12-25 19:58:19" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "platform": { + "php": ">=5.4" + }, + "platform-dev": [] +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..605c384 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,20 @@ + + + + + + + ./tests + + + + + + ./ + + ./tests + ./vendor + + + + \ No newline at end of file diff --git a/src/HeaderTagInterpreter.php b/src/HeaderTagInterpreter.php new file mode 100644 index 0000000..f0798a1 --- /dev/null +++ b/src/HeaderTagInterpreter.php @@ -0,0 +1,32 @@ +used = array(); + $this->slugify = $slugify ?: new Slugify(); + } + + // --------------------------------------------------------------- + + /** + * Slugify + * + * @param string $text + * @return string + */ + public function slugify($text) + { + $slugged = $this->slugify->slugify($text); + + $ct = 1; + $orig = $slugged; + while (in_array($slugged, $this->used)) { + $slugged = $orig . '-' . $ct; + $ct++; + } + + $this->used[] = $slugged; + return $slugged; + } +} + +/* EOF: Sluggifier.php */ \ No newline at end of file diff --git a/src/TocGenerator.php b/src/TocGenerator.php new file mode 100644 index 0000000..878785e --- /dev/null +++ b/src/TocGenerator.php @@ -0,0 +1,107 @@ + + */ +class TocGenerator +{ + use HeaderTagInterpreter; + + // --------------------------------------------------------------- + + /** + * @var \Sunra\PhpSimple\HtmlDomParser + */ + private $domParser; + + // --------------------------------------------------------------- + + /** + * Constructor + * + * @param \Sunra\PhpSimple\HtmlDomParser $domParser + */ + public function __construct(HtmlDomParser $domParser = null) + { + $this->domParser = $domParser ?: new HtmlDomParser(); + } + + // --------------------------------------------------------------- + + /** + * Get Link Items + * + * @param string $markup Content to get items from + * @param int $topLevel Top Header (1 through 6) + * @param int $depth Depth (1 through 6) + * @return array Array of items ['anchor' => 'display text', ...] + */ + public function getItems($markup, $topLevel = 1, $depth = 2) + { + // Empty? Do nothing. + if (trim($markup) == '') { + return []; + } + + // Parse HTML + $items = []; + $tags = $this->determineHeaderTags($topLevel, $depth); + $parsed = $this->domParser->str_get_html($markup); + + // Runtime exception for bad code + if ( ! $parsed) { + throw new RuntimeException("Could not parse HTML"); + } + + // Extract items + foreach ($parsed->find(implode(', ', $tags)) as $tag) { + + if ( ! $tag->id) { + continue; + } + + $dispText = $tag->title ?: $tag->plaintext; + $items[$tag->id] = $dispText; + } + + return $items; + } + + // --------------------------------------------------------------- + + /** + * Get HTML Links in list form + * + * @param string $markup Content to get items from + * @param int $topLevel Top Header (1 through 6) + * @param int $depth Depth (1 through 6) + * @return string HTML
  • items + */ + public function getHtmlItems($markup, $topLevel = 1, $depth = 2, $titleTemplate = 'Go to %s') + { + $arr = []; + + foreach ($this->getItems($markup, $topLevel, $depth) as $anchor => $displayText) { + $arr[] = sprintf( + "
  • %s
  • ", + sprintf($titleTemplate, $displayText), + $anchor, + $displayText + ); + } + + return implode('', $arr); + } +} + +/* EOF: TocGenerator.php */ \ No newline at end of file diff --git a/src/TocMarkupFixer.php b/src/TocMarkupFixer.php new file mode 100644 index 0000000..054fd21 --- /dev/null +++ b/src/TocMarkupFixer.php @@ -0,0 +1,81 @@ + + */ +class TocMarkupFixer +{ + use HeaderTagInterpreter; + + // --------------------------------------------------------------- + + /** + * @var HtmlDomParser + */ + private $domParser; + + // --------------------------------------------------------------- + + /** + * Constructor + * + * @param HtmlDomParser $domParser + */ + public function __construct(HtmlDomParser $domParser = null) + { + $this->domParser = $domParser ?: new HtmlDomParser(); + } + + // --------------------------------------------------------------- + + /** + * Fix markup + * + * @param string $markup + * @param int $topLevel + * @param int $depth + * @return string Markup with added IDs + * @throws RuntimeException + */ + public function fix($markup, $topLevel = 1, $depth = 2) + { + $sluggifier = new Sluggifier(); + + $tags = $this->determineHeaderTags($topLevel, $depth); + $parsed = $this->domParser->str_get_html($markup); + + // Runtime exception for bad code + if ( ! $parsed) { + throw new RuntimeException("Could not parse HTML"); + } + + // Extract items + foreach ($parsed->find(implode(', ', $tags)) as $tag) { + + // Ignore tags that already have IDs + if ($tag->id) { + continue; + } + + $tag->id = $sluggifier->slugify($tag->title ?: $tag->plaintext); + } + + return (string) $parsed; + } +} + +/* EOF: TocMarkupFixer.php */ \ No newline at end of file diff --git a/src/TocTwigExtension.php b/src/TocTwigExtension.php new file mode 100644 index 0000000..fad2000 --- /dev/null +++ b/src/TocTwigExtension.php @@ -0,0 +1,85 @@ + + */ +class TocTwigExtension extends Twig_Extension +{ + /** + * @var \TOC\TocGenerator + */ + private $generator; + + /** + * @var \TOC\TocMarkupFixer + */ + private $fixer; + + // --------------------------------------------------------------- + + /** + * Constructor + * + * @param \TOC\TocGenerator $generator + * @param \TOC\TocMarkupFixer $fixer + */ + public function __construct(TocGenerator $generator = null, TocMarkupFixer $fixer = null) + { + $this->generator = $generator ?: new TocGenerator(); + $this->fixer = $fixer ?: new TocMarkupFixer(); + } + + // --------------------------------------------------------------- + + public function getFilters() + { + $filters = parent::getFilters(); + + $filters[] = new \Twig_SimpleFilter('add_anchors', function($str, $top = 1, $depth = 2) { + return $this->fixer->fix($str, $top, $depth); + }); + + return $filters; + } + + // --------------------------------------------------------------- + + public function getFunctions() + { + $functions = parent::getFunctions(); + + // ~~~ + + $functions[] = new \Twig_SimpleFunction('toc', function($markup, $top = 1, $depth = 2, $titleTemplate = null) { + return ($titleTemplate) + ? $this->generator->getHtmlItems($markup, $top, $depth, $titleTemplate) + : $this->generator->getHtmlItems($markup, $top, $depth); + }); + + // ~~~ + + $functions[] = new \Twig_SimpleFunction('toc_items', function($markup, $top = 1, $depth = 2) { + return $this->generator->getItems($markup, $top, $depth); + }); + + return $functions; + } + + // --------------------------------------------------------------- + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName() + { + return 'toc'; + } +} \ No newline at end of file diff --git a/tests/TocGeneratorTest.php b/tests/TocGeneratorTest.php new file mode 100644 index 0000000..b72b3d4 --- /dev/null +++ b/tests/TocGeneratorTest.php @@ -0,0 +1,90 @@ +assertInstanceOf('\TOC\TocGenerator', $obj); + } + + // --------------------------------------------------------------- + + public function testGetItemsMatchesOnlyElementsWithIDs() + { + $obj = new TocGenerator(); + + $html = "

    A Header

    Foobar

    B Header

    C Header

    "; + $this->assertEquals(['a' => 'A Header', 'c' => 'C Header'], $obj->getItems($html, 1, 3)); + } + + // --------------------------------------------------------------- + + public function testGetItemsUsesTitleForDisplayTextWhenAvailableAndPlainTextWhenNot() + { + $obj = new TocGenerator(); + + $html = '

    A Header

    '; + $html .= '

    B Header

    '; + $html .= '

    C Header

    '; + + $this->assertEquals( + ['a' => 'Foo Bar!', 'b' => 'B Header', 'c' => 'Baz Biz~'], + $obj->getItems($html, 1, 3) + ); + } + + // --------------------------------------------------------------- + + public function testGetItemsGetsOnlyHeaderLevelsSpecified() + { + $obj = new TocGenerator(); + + $html = '

    A Header

    '; + $html .= '

    B Header

    '; + $html .= '

    C Header

    '; + $html .= '

    D Header

    '; + $html .= '
    E Header
    '; + $html .= '
    F Header
    '; + + $this->assertCount(1, $obj->getItems($html, 5, 1)); + $this->assertCount(2, $obj->getItems($html, 5, 5)); + $this->assertCount(6, $obj->getItems($html, -1, 20)); + } + + // --------------------------------------------------------------- + + public function testGetHtmlItemsReturnsExpectedListItems() + { + $obj = new TocGenerator(); + + $html = '

    A Header

    '; + $html .= '

    B Header

    '; + $html .= '

    C Header

    '; + + $this->assertEquals( + "
  • Foo Bar!
  • B Header
  • Baz Biz~
  • ", + $obj->getHtmlItems($html, 1, 3) + ); + } + + // --------------------------------------------------------------- + + public function testGetItemsReturnsAnEmptyArrayWhenNoContentOrMatches() + { + $obj = new TocGenerator(); + $this->assertEquals([], $obj->getItems("

    Boo

    Bar

    ")); + $this->assertEquals([], $obj->getItems("")); + } + +} + +/* EOF: TocGeneratorTest.php */ diff --git a/tests/TocMarkupFixerTest.php b/tests/TocMarkupFixerTest.php new file mode 100644 index 0000000..1aec5cb --- /dev/null +++ b/tests/TocMarkupFixerTest.php @@ -0,0 +1,60 @@ +assertInstanceOf('\TOC\TocMarkupFixer', $obj); + } + + // --------------------------------------------------------------- + + public function testFixAddsIdsOnlyToElementsWithoutThem() + { + $obj = new TocMarkupFixer(); + + $html = "

    No ID

    Existing ID

    Ignored

    "; + + $this->assertEquals( + '

    No ID

    Existing ID

    Ignored

    ', + $obj->fix($html, 1, 2) + ); + } + + // --------------------------------------------------------------- + + public function testFixDoesNotDuplicateIdsWhenFixing() + { + $obj = new TocMarkupFixer(); + + $html = "

    FooBar

    FooBar

    FooBar

    "; + + $this->assertEquals( + '

    FooBar

    FooBar

    FooBar

    ', + $obj->fix($html, 1, 3) + ); + } + + // --------------------------------------------------------------- + + public function testFixUsesTitleAttributeWhenAvailable() + { + $obj = new TocMarkupFixer(); + + $html = "

    No ID

    Existing ID

    Ignored

    "; + + $this->assertEquals( + '

    No ID

    Existing ID

    Ignored

    ', + $obj->fix($html, 1, 2) + ); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..3272123 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,27 @@ + + */ + +//Files to ensure exist +$checkFiles['autoload'] = __DIR__.'/../vendor/autoload.php'; +$checkFiles[] = __DIR__.'/../vendor/sunra/php-simple-html-dom-parser/README.md'; + +//Check 'Em +foreach($checkFiles as $file) { + + if ( ! file_exists($file)) { + throw new RuntimeException('Install development dependencies to run test suite.'); + } +} + +//Away we go +$autoload = require_once $checkFiles['autoload']; + +/* EOF: bootstrap.php */ \ No newline at end of file