Git Product home page Git Product logo

shortcode's Introduction

Shortcode

Build Status Latest Stable Version Total Downloads License Psalm coverage Code Coverage Scrutinizer Code Quality

Shortcode is a framework agnostic PHP library allowing to find, extract and process text fragments called "shortcodes" or "BBCodes". Examples of their usual syntax and usage are shown below:

[user-profile /]
[image width=600]
[link href="http://google.pl" color=red]
[quote="Thunderer"]This is a quote.[/quote]
[text color="red"]This is a text.[/text]

The library is divided into several parts, each of them containing logic responsible for different stages and ways of processing data:

  • parsers extract shortcodes from text and transform them to objects,
  • handlers transform shortcodes into desired replacements,
  • processors use parsers and handlers to extract shortcodes, compute replacements, and apply them in text,
  • events alter the way processors work to provide better control over the whole process,
  • serializers convert shortcodes from and to different formats like Text, XML, JSON, and YAML.

Each part is described in the dedicated section in this document.

Installation

There are no required dependencies and all PHP versions from 5.3 up to latest 8.1 are tested and supported. This library is available on Composer/Packagist as thunderer/shortcode, to install it execute:

composer require thunderer/shortcode=^0.7

or manually update your composer.json with:

(...)
"require": {
    "thunderer/shortcode": "^0.7"
}
(...)

and run composer install or composer update afterwards. If you're not using Composer, download sources from GitHub and load them as required. But really, please use Composer.

Usage

Facade

To ease usage of this library there is a class ShortcodeFacade configured for most common needs. It contains shortcut methods for all features described in the sections below:

  • addHandler(): adds shortcode handlers,
  • addHandlerAlias(): adds shortcode handler alias,
  • process(): processes text and replaces shortcodes,
  • parse(): parses text into shortcodes,
  • setParser(): changes processor's parser,
  • addEventHandler(): adds event handler,
  • serialize(): serializes shortcode object to given format,
  • unserialize(): creates shortcode object from serialized input.

Processing

Shortcodes are processed using Processor which requires a parser and handlers. The example below shows how to implement an example that greets the person with name passed as an argument:

use Thunder\Shortcode\HandlerContainer\HandlerContainer;
use Thunder\Shortcode\Parser\RegularParser;
use Thunder\Shortcode\Processor\Processor;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;

$handlers = new HandlerContainer();
$handlers->add('hello', function(ShortcodeInterface $s) {
    return sprintf('Hello, %s!', $s->getParameter('name'));
});
$processor = new Processor(new RegularParser(), $handlers);

$text = '
    <div class="user">[hello name="Thomas"]</div>
    <p>Your shortcodes are very good, keep it up!</p>
    <div class="user">[hello name="Peter"]</div>
';
echo $processor->process($text);

Facade example:

use Thunder\Shortcode\ShortcodeFacade;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;

$facade = new ShortcodeFacade();
$facade->addHandler('hello', function(ShortcodeInterface $s) {
    return sprintf('Hello, %s!', $s->getParameter('name'));
});

$text = '
    <div class="user">[hello name="Thomas"]</div>
    <p>Your shortcodes are very good, keep it up!</p>
    <div class="user">[hello name="Peter"]</div>
';
echo $facade->process($text);

Both result in:

    <div class="user">Hello, Thomas!</div>
    <p>Your shortcodes are very good, keep it up!</p>
    <div class="user">Hello, Peter!</div>

Configuration

Processor has several configuration options available as with*() methods which return the new, changed instance to keep the object immutable.

  • withRecursionDepth($depth) controls the nesting level - how many levels of shortcodes are actually processed. If this limit is reached, all shortcodes deeper than level are ignored. If the $depth value is null (default value), nesting level is not checked, if it's zero then nesting is disabled (only topmost shortcodes are processed). Any integer greater than zero sets the nesting level limit,
  • withMaxIterations($iterations) controls the number of iterations that the source text is processed in. This means that source text is processed internally that number of times until the limit was reached or there are no shortcodes left. If the $iterations parameter value is null, there is no iterations limit, any integer greater than zero sets the limit. Defaults to one iteration,
  • withAutoProcessContent($flag) controls automatic processing of shortcode's content before calling its handler. If the $flag parameter is true then handler receives shortcode with already processed content, if false then handler must process nested shortcodes itself (or leave them for the remaining iterations). This is turned on by default,
  • withEventContainer($events) registers event container which provides handlers for all the events fired at various stages of processing text. Read more about events in the section dedicated to them.

Events

If processor was configured with events container there are several possibilities to alter the way shortcodes are processed:

  • Events::FILTER_SHORTCODES uses FilterShortcodesEvent class. It receives current parent shortcode and array of shortcodes from parser. Its purpose is to allow modifying that array before processing them,
  • Events::REPLACE_SHORTCODES uses ReplaceShortcodesEvent class and receives the parent shortcode, currently processed text, and array of replacements. It can alter the way shortcodes handlers results are applied to the source text. If none of the listeners set the result, the default method is used.

There are several ready to use event handlers in the Thunder\Shortcode\EventHandler namespace:

  • FilterRawEventHandler implements FilterShortcodesEvent and allows to implement any number of "raw" shortcodes whose content is not processed,
  • ReplaceJoinEventHandler implements ReplaceShortcodesEvent and provides the mechanism to apply shortcode replacements by discarding text and returning just replacements.

The example below shows how to manually implement a [raw] shortcode that returns its verbatim content without calling any handler for nested shortcodes:

use Thunder\Shortcode\Event\FilterShortcodesEvent;
use Thunder\Shortcode\EventContainer\EventContainer;
use Thunder\Shortcode\Events;
use Thunder\Shortcode\HandlerContainer\HandlerContainer;
use Thunder\Shortcode\Parser\RegularParser;
use Thunder\Shortcode\Processor\Processor;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;

$handlers = new HandlerContainer();
$handlers->add('raw', function(ShortcodeInterface $s) { return $s->getContent(); });
$handlers->add('n', function(ShortcodeInterface $s) { return $s->getName(); });
$handlers->add('c', function(ShortcodeInterface $s) { return $s->getContent(); });

$events = new EventContainer();
$events->addListener(Events::FILTER_SHORTCODES, function(FilterShortcodesEvent $event) {
    $parent = $event->getParent();
    if($parent && ($parent->getName() === 'raw' || $parent->hasAncestor('raw'))) {
        $event->setShortcodes(array());
    }
});

$processor = new Processor(new RegularParser(), $handlers);
$processor = $processor->withEventContainer($events);

assert(' [n /] [c]cnt[/c] ' === $processor->process('[raw] [n /] [c]cnt[/c] [/raw]'));
assert('n true  [n /] ' === $processor->process('[n /] [c]true[/c] [raw] [n /] [/raw]'));

Facade example:

use Thunder\Shortcode\Event\FilterShortcodesEvent;
use Thunder\Shortcode\Events;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
use Thunder\Shortcode\ShortcodeFacade;

$facade = new ShortcodeFacade();
$facade->addHandler('raw', function(ShortcodeInterface $s) { return $s->getContent(); });
$facade->addHandler('n', function(ShortcodeInterface $s) { return $s->getName(); });
$facade->addHandler('c', function(ShortcodeInterface $s) { return $s->getContent(); });

$facade->addEventHandler(Events::FILTER_SHORTCODES, function(FilterShortcodesEvent $event) {
    $parent = $event->getParent();
    if($parent && ($parent->getName() === 'raw' || $parent->hasAncestor('raw'))) {
        $event->setShortcodes(array());
    }
});

assert(' [n /] [c]cnt[/c] ' === $facade->process('[raw] [n /] [c]cnt[/c] [/raw]'));
assert('n true  [n /] ' === $facade->process('[n /] [c]true[/c] [raw] [n /] [/raw]'));

Parsing

This section discusses available shortcode parsers. Regardless of the parser that you will choose, remember that:

  • shortcode names can be only aplhanumeric characters and dash -, basically must conform to the [a-zA-Z0-9-]+ regular expression,
  • unsupported shortcodes (no registered handler or default handler) will be ignored and left as they are,
  • mismatching closing shortcode ([code]content[/codex]) will be ignored, opening tag will be interpreted as self-closing shortcode, eg. [code /],
  • overlapping shortcodes ([code]content[inner][/code]content[/inner]) will be interpreted as self-closing, eg. [code]content[inner /][/code], second closing tag will be ignored,

There are three included parsers in this library:

  • RegularParser is the most powerful and correct parser available in this library. It contains the actual parser designed to handle all the issues with shortcodes like proper nesting or detecting invalid shortcode syntax. It is slightly slower than regex-based parser described below,
  • RegexParser uses a handcrafted regular expression dedicated to handle shortcode syntax as much as regex engine allows. It is fastest among the parsers included in this library, but it can't handle nesting properly, which means that nested shortcodes with the same name are also considered overlapping - (assume that shortcode [c] returns its content) string [c]x[c]y[/c]z[/c] will be interpreted as xyz[/c] (first closing tag was matched to first opening tag). This can be solved by aliasing handler name, because for example [c]x[d]y[/d]z[/c] will be processed correctly,
  • WordpressParser contains code copied from the latest currently available WordPress (4.3.1). It is also a regex-based parser, but the included regular expression is quite weak, it for example won't support BBCode syntax ([name="param" /]). This parser by default supports the shortcode name rule, but can break it when created with one of the named constructors (createFromHandlers() or createFromNames()) that change its behavior to catch only configured names. All of it is intentional to keep the compatibility with what WordPress is capable of if you need that compatibility.

Syntax

All parsers (except WordpressParser) support configurable shortcode syntax which can be configured by passing SyntaxInterface object as the first parameter. There is a convenience class CommonSyntax that contains default syntax. Usage is shown in the examples below:

use Thunder\Shortcode\HandlerContainer\HandlerContainer;
use Thunder\Shortcode\Parser\RegexParser;
use Thunder\Shortcode\Parser\RegularParser;
use Thunder\Shortcode\Processor\Processor;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
use Thunder\Shortcode\Syntax\CommonSyntax;
use Thunder\Shortcode\Syntax\Syntax;
use Thunder\Shortcode\Syntax\SyntaxBuilder;

$builder = new SyntaxBuilder();

Default syntax (called "common" in this library):

$defaultSyntax = new Syntax(); // without any arguments it defaults to common syntax
$defaultSyntax = new CommonSyntax(); // convenience class
$defaultSyntax = new Syntax('[', ']', '/', '=', '"'); // created explicitly
$defaultSyntax = $builder->getSyntax(); // builder defaults to common syntax

Syntax with doubled tokens:

$doubleSyntax = new Syntax('[[', ']]', '//', '==', '""');
$doubleSyntax = $builder // actually using builder
    ->setOpeningTag('[[')
    ->setClosingTag(']]')
    ->setClosingTagMarker('//')
    ->setParameterValueSeparator('==')
    ->setParameterValueDelimiter('""')
    ->getSyntax();

Something entirely different just to show the possibilities:

$differentSyntax = new Syntax('@', '#', '!', '&', '~');

Verify that each syntax works properly:

$handlers = new HandlerContainer();
$handlers->add('up', function(ShortcodeInterface $s) {
    return strtoupper($s->getContent());
});

$defaultRegex = new Processor(new RegexParser($defaultSyntax), $handlers);
$doubleRegex = new Processor(new RegexParser($doubleSyntax), $handlers);
$differentRegular = new Processor(new RegularParser($differentSyntax), $handlers);

assert('a STRING z' === $defaultRegex->process('a [up]string[/up] z'));
assert('a STRING z' === $doubleRegex->process('a [[up]]string[[//up]] z'));
assert('a STRING z' === $differentRegular->process('a @up#string@!up# z'));

Serialization

This library supports several (un)serialization formats - XML, YAML, JSON and Text. Examples below shows how to both serialize and unserialize the same shortcode in each format:

use Thunder\Shortcode\Serializer\JsonSerializer;
use Thunder\Shortcode\Serializer\TextSerializer;
use Thunder\Shortcode\Serializer\XmlSerializer;
use Thunder\Shortcode\Serializer\YamlSerializer;
use Thunder\Shortcode\Shortcode\Shortcode;

$shortcode = new Shortcode('quote', array('name' => 'Thomas'), 'This is a quote!');

Text:

$text = '[quote name=Thomas]This is a quote![/quote]';
$textSerializer = new TextSerializer();

$serializedText = $textSerializer->serialize($shortcode);
assert($text === $serializedText);
$unserializedFromText = $textSerializer->unserialize($serializedText);
assert($unserializedFromText->getName() === $shortcode->getName());

JSON:

$json = '{"name":"quote","parameters":{"name":"Thomas"},"content":"This is a quote!","bbCode":null}';
$jsonSerializer = new JsonSerializer();
$serializedJson = $jsonSerializer->serialize($shortcode);
assert($json === $serializedJson);
$unserializedFromJson = $jsonSerializer->unserialize($serializedJson);
assert($unserializedFromJson->getName() === $shortcode->getName());

YAML:

$yaml = "name: quote
parameters:
    name: Thomas
content: 'This is a quote!'
bbCode: null
";
$yamlSerializer = new YamlSerializer();
$serializedYaml = $yamlSerializer->serialize($shortcode);
assert($yaml === $serializedYaml);
$unserializedFromYaml = $yamlSerializer->unserialize($serializedYaml);
assert($unserializedFromYaml->getName() === $shortcode->getName());

XML:

$xml = '<?xml version="1.0" encoding="UTF-8"?>
<shortcode name="quote">
  <bbCode/>
  <parameters>
    <parameter name="name"><![CDATA[Thomas]]></parameter>
  </parameters>
  <content><![CDATA[This is a quote!]]></content>
</shortcode>
';
$xmlSerializer = new XmlSerializer();
$serializedXml = $xmlSerializer->serialize($shortcode);
assert($xml === $serializedXml);
$unserializedFromXml = $xmlSerializer->unserialize($serializedXml);
assert($unserializedFromXml->getName() === $shortcode->getName());

Facade also supports serialization in all available formats:

use Thunder\Shortcode\Shortcode\Shortcode;
use Thunder\Shortcode\ShortcodeFacade;

$facade = new ShortcodeFacade();

$shortcode = new Shortcode('name', array('arg' => 'val'), 'content', 'bbCode');

$text = $facade->serialize($shortcode, 'text');
$textShortcode = $facade->unserialize($text, 'text');
assert($shortcode->getName() === $textShortcode->getName());

$json = $facade->serialize($shortcode, 'json');
$jsonShortcode = $facade->unserialize($json, 'json');
assert($shortcode->getName() === $jsonShortcode->getName());

$yaml = $facade->serialize($shortcode, 'yaml');
$yamlShortcode = $facade->unserialize($yaml, 'yaml');
assert($shortcode->getName() === $yamlShortcode->getName());

$xml = $facade->serialize($shortcode, 'xml');
$xmlShortcode = $facade->unserialize($xml, 'xml');
assert($shortcode->getName() === $xmlShortcode->getName());

Handlers

There are several builtin shortcode handlers available in Thunder\Shortcode\Handler namespace. Description below assumes that given handler was registered with xyz name:

  • NameHandler always returns shortcode's name. [xyz arg=val]content[/xyz] becomes xyz,
  • ContentHandler always returns shortcode's content. It discards its opening and closing tag. [xyz]code[/xyz] becomes code,
  • RawHandler returns unprocessed shortcode content. Its behavior is different than FilterRawEventHandler because if content auto processing is turned on, then nested shortcodes handlers were called, just their result was discarded,
  • NullHandler completely removes shortcode with all nested shortcodes,
  • DeclareHandler allows to dynamically create shortcode handler with name as first parameter that will also replace all placeholders in text passed as arguments. Example: [declare xyz]Your age is %age%.[/declare] created handler for shortcode xyz and when used like [xyz age=18] the result is Your age is 18.,
  • EmailHandler replaces the email address or shortcode content as clickable mailto: link:
  • PlaceholderHandler replaces all placeholders in shortcode's content with values of passed arguments. [xyz year=1970]News from year %year%.[/xyz] becomes News from year 1970.,
  • SerializerHandler replaces shortcode with its serialized value using serializer passed as an argument in class' constructor. If configured with JsonSerializer, [xyz /] becomes {"name":"json", "arguments": [], "content": null, "bbCode": null}. This could be useful for debugging your shortcodes,
  • UrlHandler replaces its content with a clickable link:
    • [xyz]http://example.com[/xyz] becomes <a href="http://example.com">http://example.com</a>,
    • [xyz="http://example.com"]Visit my site![/xyz] becomes <a href="http://example.com">Visit my site!</a>,
  • WrapHandler allows to specify the value that should be placed before and after shortcode content. If configured with <strong> and </strong>, the text [xyz]Bold text.[/xyz] becomes <strong>Bold text.</strong>.

Contributing

Want to contribute? Perfect! Submit an issue or Pull Request and explain what would you like to see in this library.

License

See LICENSE file in the main directory of this library.

shortcode's People

Contributors

awilum avatar dmytro-y-dev avatar funkjedi avatar leonardoalifraco avatar thunderer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

shortcode's Issues

BBCode - Converting to HTML (for beginners)

Hi,

I am creating a simple forum using Laravel PHP Framework. Currently, for converting BBCode to HTML I am using the following package: https://github.com/Genert/bbcode

This package uses regex to detect and convert BBCode, for example:

        // Add horizontal rule - [hr]
        $bbCode->addParser(
            'hr', // name of the "parser"
            '/\[hr\]/', // BBCode to detect (regex)
            '<hr>', // convert to HTML
            ''
        );

        // Image
        $bbCode->addParser(
            'image',
            '/\[img\](.*?)\[\/img\]/s',
            '<img class="img-fluid" src="$1">',
            '$1' // content
        );

        // Youtube
        $bbCode->addParser(
            'youtube',
            '/\[youtube\](.*?)\[\/youtube\]/s',
            '<div class="videoWrapper"><iframe src="//www.youtube.com/embed/$1" frameborder="0" allowfullscreen></iframe></div>',
            '$1'
        );

        // Add emoticons:
        $icons = [
            ':)' => 'smile',
            ':angel:' => 'angel',
            ':angry:' => 'angry',
            '8-)' => 'cool',
        ];

        foreach ($icons as $icon => $name) {
            $icon = preg_quote($icon);
            $bbCode->addParser(
                "$name",
                "/$icon/s",
                "<img class='' src='/test/javascripts/sceditor/emoticons/$name.png' alt='$name' title='$name'/>",
                ""
            );
        }
// . . . AND SO ON . . .

I am having some problems with this (nesting or link) and on Stackoverflow I saw that you have recommended this package, and I have a few beginner questions:

1. If I understood correctly, after installing you will have to add rules for converting BBCode to HTML (it doesn't have any rules for converting by itself)?

2. Can you give me a few examples (starting points - how to register rules) for converting BBCode to HTML, for example for:

  • [quote]...[/quote]
  • [quote=author]...[/quote]
  • Emojis (e.g. :) or :alien:)

Thanks!

Semver

Hey there,

i know the project is in 0.* stadium.
But I'm using it for quite some time and it seems stable to me.
Would you mind to tag a 1.0 release somehow or how is the plan?

My problem is: I'm working on a php5.3 project (yep, really that old) and I can't influence the maintainers. So a "not semver" piece of software is very dangerous.
I can fix the version, but I'm not sure if this is good in that case.

4ac3b1b
could break that project.

Should i lock the version or do you plan to release a mayor version somehow?
Or do you follow an other (maybe ~) version policy?

Kind regards
Patrick

BBCode shortcode parameter value chokes on the tag closing character

Using CommonSyntax, the following text won't be picked up by the regular parser:
[url=http://example.com]link[/url]

The parser correctly recognize the opening shortcode, continue to getting the parameter value, then finds a /which is a marker token, then look for a closing token, doesn't find it and return false.

Using parameter delimiter like this [url="http://example.com"]link[/url] yields the expected result.

Exception when trying to get missing shortcode parameter

Current implementation of AbstractShortcode throws an exception when trying to get parameter of the shortcode which is not present. Method getParameter($name, $default) detects if default value was provided by checking if $default value is null so that when someone explicitly passes null exception is thrown. Two possible solutions of this problem are as follows:

  • do not throw an exception at all, just return the $default,
  • check for number of function arguments and throw exception only if default value is null and func_num_args() < 2.

Method getParameter returns value with quotes

Parameter values using double quotes get passed back with double quotes when using getParameter. Maybe there's a way to modify this configuration to strip double quotes?

Environment

  • PHP v7.2
  • Shortcode v0.6.5
$handlers = new HandlerContainer();
$handlers->add('hello', function (ShortcodeInterface $s) {
    return sprintf('Hello, %s', $s->getParameter('name'));
});
$processor = new Processor(new WordpressParser(), $handlers);
return $processor->process($input);

This example shows two of the same shortcodes: one with double quotes for the parameter value and one without.

Philosophy: Allow extending RegularParser

Hi @thunderer ,

I was wondering if there was a specific reason for making the RegularParser class final and most of its methods private instead of protected?

The RegularParser is almost what I need but I would like to override the shotcode function, and I can't do it currently without modifying your library. The other option is to duplicate all the private code and I'm not too enthusiastic about it.

What do you think?

Handling self-closing shortcode without handler

In the current implementation of Processor, TextSerializer is used to serialize shortcodes without registered handler. This may cause bugs when for example text contains consecutive shortcodes of the same name. Let's say that we have text with two shortcodes, one self-closed and one with content: [code /] [code]content[/code] and it does not have a registered handler. The result of the processing will be: [code] [code]content[/code] because said serializer does not have the information about shortcode being self-closing or not. Then if this text gets passed again into Processor the result will be a single shortcode with content [code]content. Possible solutions to this problem:

  • force self-closing slash on all shortcodes without content (content equal to null),
  • expand RegexParser to also extract information about that "slash" and ParsedShortcode to store it as a separate field.

Two shortcodes in a row does not work?

Error

Maybe I'm missing a setting or something but I can't get the thing below to work properly:

<?php
include 'vendor/autoload.php';

use Thunder\Shortcode\ShortcodeFacade;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;

$facade = new ShortcodeFacade();
$facade->addHandler('hello', function(ShortcodeInterface $s) {
    return sprintf('Hello, %s!', $s->getParameter('name'));
});

$text = '
    <div class="users">
    [hello name="Thomas"]
    [hello name="Peter"]
    </div>
';
echo $facade->process($text);

The result will be:

<div class="users">
Hello, Thomas!
[hello name="Peter"]
</div>

Works

So when wrapping both shortcodes around a single div it will only parse the first one.

The strange thing is that this works:

(se full example above. I replaced the $text variable)

$text = '
    [hello name="Thomas"]
    [hello name="Peter"]
';

The result of the last test is:

Hello, Thomas!
Hello, Peter!

Handler events

It would be cool to be able to register events at various parts of the text processing flow, for example:

  • PRE_PROCESS_TEXT - input text,
  • PRE_EXTRACT - match position and string,
  • POST_EXTRACT - ExtractorMatch instance,
  • PRE_PARSE - string match,
  • POST_PARSE - ShortcodeInterface instance,
  • PRE_PROCESS - handler and ShortcodeInterface instance,
  • POST_PROCESS - result of the handler,
  • POST_PROCESS_TEXT - output text.

Some of them do not make sense, but I wanted to dump all possible ideas. These events could be used to properly implement things present in other libraries like adding links to plain URLs and emails or handling emoticons.

Processor would be in charge of firing those events through EventDispatcher (optional constructor argument) which would be configured just like HandlerContainer.

Parsing wildcard tag like [banner-*]

Hi there!

I have a 300+ documents having a set of shortcodes like [banner-top], [banner-bottom] and so on. I do not know the full set of these shortcodes but I know the naming convention [banner-*] where * is fitting [a-z-_] regex. All I need is to register them in DB and replace with another shortcode like [banner type="top"].

Is it possible to define one handler for whole set of these shortcodes without collecting them manually before actual processing with your lib?

RegularParser = Uncaught Error: Maximum function nesting level of '256' reached, aborting!

We're using thunderer/shortcode in our Grav CMS shortcode-core plugin: https://github.com/getgrav/grav-plugin-shortcode-core

And it was reported to me that while using also using the Gantry5 Plugin while Shortcode Core was active, resulted in an infinite loop bug. I dug into this a bit and it appears that some HTML code output by the plugin causes this PHP infinite loop bug when using the RegularParser. It works fine with the RegexParser or WordpressParser BTW.

I have created a little test project where I'm using JUST the thunderer/shortcode library and the sample content in question, and it's definitely repeatable even out of the context of Grav and the Shortcode Core plugin.

shortcode-infinite-loop.zip

In this file, you can see the HTML that is causing the issue in question. I've tried 'cleaning' the HTML, but that does not change the results.

My hunch is that it's the HTML data attributes that are throwing the parser for a loop:

...
<li>
    <a
        data-g5-ajaxify
        data-g5-ajaxify-target="[data-g5-content]"
        href="/testing/grav-g5/admin/gantry/positions?nonce=85998f180935f47b4b01ad293ac1325c"
        ><i class="fa fa-fw fa-object-group" aria-hidden="true"></i>
        Positions</a
    >
</li>
<li>
    <a
        data-g5-ajaxify
        data-g5-ajaxify-target="[data-g5-content]"
        href="/testing/grav-g5/admin/gantry/menu?nonce=85998f180935f47b4b01ad293ac1325c"
        ><i class="fa fa-fw fa-bars" aria-hidden="true"></i>
        <span>Menu</span></a
    >
</li>
...

I'm inclined to disable the Grav Shotcode Core while Gantry5 plugin is processing, as it's really not being used here, it's just that Grav is processing ALL page content for potential shortcodes.

RegularParser not working with HTML content

The Regular parser is not working when having HTML content on shortcodes.

Example:

$handlers->add($emptyCustomTag, function (ShortcodeInterface $s) {
    return '';
});
$processor = new Processor(new RegularParser(), $handlers);
$processor->process("[isolated-block]<a>Hola</a>[/isolated-block]");

Returns

<a>Hola</a>[/isolated-block]

Custom Short Codes

I'm yet to use this library but I was wondering is Custom Shortcodes being supported? (#16 "add ProcessedShortcodeInterface and Processor::withShortcodeFactory() (think of a better name) to allow creating custom shortcode objects")

Underscores in tag names not matched

Wasn't sure if this was a deliberate decision but in parsing tags containing underscores aren't matched.

eg: [testtag] works but [test_tag] does not.

Both the regular parser here:
https://github.com/thunderer/Shortcode/blob/master/src/Parser/RegularParser.php#L81
and the Wordpress parser here:
https://github.com/thunderer/Shortcode/blob/master/src/Parser/WordpressParser.php#L27

use a slightly different match syntax but neither contain the _ character.

Reason I ask is because I'm trying to use this library to make a content importer from Wordpress and Wordpress does seem to parse them correctly.

Shortcodes ignored with multiple nested levels

Hi,

First, thank you for making this library available. It appears very comprehensive, but I think I've found an issue that I am banging my head against for the past 3 days. In short, I believe that this code starts to omit shortcodes when there are several levels of. Please see my input text with shortcodes below:

[la-row] [la-column width="100%"] [la-text format="h1"]Welcome![/la-text] [la-text]This page allows you to send commands to LightAct. These commands are simple strings that can be read with LightAct Layer Layouts and acted upon. This page uses standard web technologies such as html, Jquery, and AJAX so, if you can use these frameworks, you can write your own page. You can also use our own page builder, which you can access at[/la-text] [/la-column] [/la-row] [la-row] [la-column width="25%"] [la-text format="h4"]Sample heading 3[/la-text] [la-text]Sample text[/la-text] [/la-column] [la-column width="25%"] [la-text format="h4"]Sample heading 3[/la-text] [la-text]Sample text[/la-text] [/la-column] [la-column width="25%"] [la-text format="h4"]Sample heading 3[/la-text] [la-text]Sample text[/la-text] [/la-column] [la-column width="25%"] [la-text format="h4"]Sample heading 3[/la-text] [la-text]Sample text[/la-text] [/la-column] [/la-row]

I've written a php code using your library which transforms the above text into this webpage. Up to here it all works fine.
example1

But if I multiply the above shortcodes 4 times, the last couple of columns start to get omitted as shown here.
example2

Now this is only one manifestation of this issue. From my experience, the more shortcodes there are, especially if they are nested, the sooner this problem appears.

I was wondering if there is anything you can do to help?

Ideas

Just a list of issues to remember:

  • custom exceptions for specific cases rather than using generic ones,
  • recipes for specific tasks (like find all shortcode names in text),
  • upgrade RegularParser to not split text into tokens at once but iterate over characters,
  • shortcodes with children to represent their structure in text,
  • shortcode's getTextUntilNext() which returns the content up to the next shortcode opening tag,
  • shortcode name wildcard,
  • more complex positional parameters [x=http://x.com], [x x=http://x.com],
  • shortcode escaping by doubling opening / closing token [[x /]], [[y a=b]],
  • tools for text analysis (nesting depth, count, name stats, etc.)
  • configurable shortcode name validation rules,
  • solve BBCode ambiguity issues in [x=] and [x= arg=val],
  • add ProcessedShortcodeInterface and Processor::withShortcodeFactory() (think of a better name) to allow creating custom shortcode objects using ProcessorContext that are compatible with ProcessedShortcode. This will allow users to put their information inside while still maintaining how library works,
  • fix inconsistency between getText and getShortcodeText between ParsedShortcode and ProcessedShortcode,
  • make ShortcodeFacade a mutable class with all the shortcuts to ease library usage (#36),
  • new regex parser that catches only opening/closing tags and handles nesting manually,
  • repeated shortcodes, ie. ability to repeat inner content with collections of data, [list]- [item/],[/list] which renders multiple "item" elements, children of list (item shortcodes) receive context from data passed to list,
  • better documentation than it is now in README (#34),
  • extract several event listener classes with __invoke() to ease events usage (#33),
  • find a way to strip content outside shortcodes in given shortcode content without losing tree structure (apply results event),
  • configurable handler for producing Processor::process() return value using array of replacements (events FTW),
  • issue with losing shortcode parent when many same shortcodes are on the same level,
  • HUGE BC (just an idea, no worries): Change ProcessorInterface::process() to receive array of parsed shortcodes to allow greater flexibility (eg. filtering by parent),
  • find a way to retrieve original shortcode content even when auto-processing,
  • prevent the ability to create shortcode with falsy name (require non-empty string value),
  • resolve naming problem for shortcode parameters (current) vs. arguments,
  • suggest symfony/yaml in composer.json for YAML serializer,
  • allow escaping with double open/close tags like in WordPress ([[code]value[/code]]),
  • add Wordpress parser (regex parser with code from WP).

Regular parser:

  • fix BBCode parsing (unused variable).

BBCode:

  • decide whether bbcode value should be:
    • merged with parameters as parameter with shortcode name, no API change, easier version,
    • a separate element, fourth parameter in Shortcode constructor, and separate getter,
    • a separate shortcode type (requires changes in both Parsed and Processed as well).

Built-in handlers:

  • DeclareHandler should typehint interface in constructor, (needs add() method)
  • EmailHandler could be a BBCode,
  • PlaceholderHandler should have configurable placeholder braces,
  • UrlHandler could be a BBCode,
  • WrapHandler could have several most common variants (eg. bold) created as named constructors.

Issue with unregistered shortcode

Hi guys,

Thanks for this package, it is very valuable for integrating Wordpress with other platforms.

I'm having an issue when I try to replace only a set of shortcodes, specifically with the non handled ones that have "special" characters.

Example:

$handlers = new HandlerContainer();
$processor = new Processor(new RegularParser(), $handlers);
$sampleValue = '[listing-link id="12345"]Holá[/listing-link]';
$processedValue = $processor->process($sampleValue);
$this->assertEquals($sampleValue, $processedValue);

The test fails and the processedValue has:

[listing-link id="12345"]Holá?[/listing-link]

Instead of returning the same string.

I've already tried with the Regular and Wordpress parsers, and the behaviours are the same.

Right now I'm solving the issue by creating a handler that returns the same shortcode, but that's far from the expected functionality.

`[0]` will throw an exception with RegexParser

We have a page that has some JS pasted in it.. part of that JS has the string [0] and this throws the error:

InvalidArgumentException Shortcode name must be a non-empty string!

crikey there was an error 2016-01-25 17-36-19

Test code:

[0] something

FYI WordpressParser works fine.

Performance Issue with `RegularParser`

As outlined in the original PR comment: #26 (comment), I was testing the Shortcode against some documentation I have for Grav CMS where I have created a Tab shortcode. I was running into a couple of issues, but one of them is related to very slow parsing of this example page with the RegularParser.

After a few back and discussions with @thunderer, he believes it is related to the various non-shortcode [] references contained in the document.

I have put together a simple test scenario (https://github.com/rhukster/shortcode-test) that shows this issue. There are two documents, a small one that is considerably slower than either the RegexParser or the WordpressParser. The RegularParser is not able to even process the full document as it just continues to spin until the PHP process is terminated.

FYI Both Wordpress and Regex parsers are able to parse this document. (note: corruption issues are handled in a separate issue #25 (comment))

PHP 7.4 issues

In src/Processor/Processor.php line 143:

  • mb_strrpos(): Passing the encoding as third parameter is deprecated. Use an explicit zero offset

Why not using Repositories Pattern ?

Hello there,

Since the code has already Interfaces and Managers, you should use a repository pattern to organize and centralize the whole code.

It'll also remove any chances of duplications and make the code less complex to use including the fact that you will only need to inject a single depency instead of 5, as exemple :

use Thunder\Shortcode\Extractor;
use Thunder\Shortcode\Parser;
use Thunder\Shortcode\Processor;
use Thunder\Shortcode\Shortcode;
use Thunder\Shortcode\Serializer\JsonSerializer;

$processor = new Processor(new Extractor(), new Parser());
$processor->addHandler('sample', function(Shortcode $s) {    
    return (new JsonSerializer())->serialize($s);
    });
assert('x {"name":"sample","args":{"arg":"val"},"content":"cnt"} y'
    === $processor->process('x [sample arg=val]cnt[/sample] y');

Will be like :

use Thunder\Shortcode\ShortcodeManager;
$instance = new ShortcodeManager();
$instance->processor()->addHandler('sample', function(Shortcode $s) {    
    return $instance->serializer()->serialize($s)->toJson();
    });
assert('x {"name":"sample","args":{"arg":"val"},"content":"cnt"} y' 
    === $instance->processor()->process('x [sample arg=val]cnt[/sample] y');

Also, it'll allow us to make a globalized serializer that'll serialize to JSON, XML and more ;)

You can think about changing some method to make these more simple to use with a repository, all the dependencies are already injected and ready to use.

Parse error with Vietnamese characters or open and close tag only

Hello,
Thank you because this library.
But I have some issues with some content below:

  1. Issue with Vietnamese characters:
[container]
[Tiêu đề]
[/container]

In this case I want to show <div class="container">[Tiêu đề]</div> ("Tiêu đề" is "Title" in English) but the container shortcode auto close and I get <div class="container"></div>[Tiêu đề][/container]

But this content will working if it is not contain Vietnamese characters:

[container]
[Tieu de]
[/container]
  1. Issue with open and close tag only
[container]
[]
[/container]

In this case I want to show <div class="container">[]</div> but the container shortcode auto close and I get <div class="container"></div>[][/container]. Similar to issue one.

Can you help me fix these issues?

Thank you.

[*] Asterisk not allowed/working handle name?

Hi, thanks for the project! I like it very much so far. Nonetheless, I'm having a hard Time registering some handles. Trying to register [*] as handle Name doesn't result in an Exception but neither does it seem to result in a working handle. It is not parsed from the input. For example:

$facade = new ShortcodeFacade();
$facade->addHandler('*', function(ShortcodeInterface $s) {
    return '<li>' .$s->getContent() .'</li>';
});
echo $facade->process('[*]Hello World[/*]');

results in:

[*]Hello World[/*]

I tried escaping the asterisk as well. Is it me doing something wrong? I mean [*] is a pretty standard BBCode Element, isn't it? Would be a pain to replace it in WYSIWYG editors for this reason.

Thanks, Mark.

Nested Shortcode for foreach loop

Hello, I have a problem. I need to show the contents of the array using foreach() with the shortcode, like:

[customers]
Name - [cust_name]
Contact - [cust_contact]
[/customers]

How I can achieve this? Please help me. Thanks.

Wrong parent shortcode returned in nested shortcodes

Hello,
I'm dealing with nested shortcodes and I faced the following issue. Giving this shortcode structure:

[shortcode1]
    [shortcode2]
        [shortcode3]
        [/shortcode3]
    [/shortcode2]
[/shortcode1]

calling getParent() on shortcode3 returns the shortcode1 and not the expected shortcode2.

Question: Best way to handle a `[raw][/raw]` shortcode?

I'm trying to find the best way to write a [raw][/raw] shortcode that stops the Shortcode library from processing anything between these raw tags. My current implementation fakes it by taking setting the 'text' back to the original unmodified text. However, this doesn't stop Shortcode from processing that inner stuff first.

    private function addRawHandler()
    {
        $this->handlers->add('raw', function(ShortcodeInterface $shortcode) {
            $raw = trim(preg_replace('/\[raw\](.*?)\[\/raw\]/is','${1}', $shortcode->getShortcodeText()));
            return $raw;
        });
    }

This raised it's head when I ran into the [0] bug in some javascript example code on my page. While a fix was quickly found, a situation could come up where a fix is impossible or not practical. Turning off shortcodes processing in parts of a page is therefore an important function to have. What is the better way of doing this?

Cheers!

[] Does not work inside a shortcode

I've been trying really hard to break this thing. It was not easy but I found a way to break it.

Shortcodes like this will not work:

[characters]
    []
[/characters]

If the brackets has characters like [sada], the characters shortcode works as expected. It's only when it's empty it does not work. In that case it just leave it unparsed.

On the upside I tried '"_åäö and other strange characters which works fine.

mb_strrpos() 3rd parameter deprecated in PHP 7.4

Deprecated: mb_strrpos(): Passing the encoding as third parameter is deprecated.
Use an explicit zero offset in Processor\Processor.php on line 139

Passing the encoding as 3rd parameter to mb_strrpos() is deprecated.
Instead pass a 0 offset, and encoding as 4th parameter.

I made a new Shortcode library - Inspired by yours

I wanted to try my own thing, so I created an alternative to your Shortcode library.

https://github.com/jenstornell/php-shortcode

I link to your library in the readme file, as an inspiration.

Differences

There are some main differences. I will just bring up the differences.

thunderer/shortcode

  • Allows content between shortcode start and end tags like [shortcode]Content[/shortcode].
  • Very advanced in general when it comes to configuration, parsing, processing, syntax and serialization and handlers.

jenstornell/php-shortcode

  • Only a single file
  • Very easy to use
  • Supports literal quotes

I was inspired by your library as well as WordPress Shortcode API. If you find inspiration from my library, you are welcome to snag a few ideas.

Builtin example handlers

For convenience and reducing the amount of code needed to be written by end user, this project should include some handlers ready to use or with minimal configuration. Since handling of raw closures would be difficult, those handlers should be made using __invoke() magic method. Several examples to work on:

  • RawHandler always returns getTextContent() to provide "raw" shortcode handler,
  • WrapHandler($before, $after) returns shortcode content with $before and $after added in respective places,
  • NameHandler() returns shortcode name,
  • DeclareHandler() allows using constructs like [declare age value=18]Your age is %value%.[/declare] and then automatically handle constructs like [age], [age value=20] and so on by adding handler to processor which replaces all parameters into content placeholders with honoring default values from declare,
  • ContentHandler() return shortcode content,
  • UrlHandler() converts shortcode content to an url,
  • EmailHandler() converts shortcode content to a clickable email mailto: link,
  • CallbackHandler($callback) passes ShortcodeInterface to given callback and returns its result (but you can pass the callback directly, so it's somewhat useless),
  • PlaceholderHandler() replaces %placeholders% using shortcode arguments inside its content,
  • EmbedHandler($type) embeds posts from given 3rd party website like Facebook, Twitter, YouTube and so on,
  • NullHandler() default handler usable when you want to discard a shortcode or provide default handler to Processor,
  • SerializerHandler(SerializerInterface $serializer) returns serialized version of passed shortcode.

Multiple options in one add handler

Hello, we will be having a use case when we need to add similar behaviour to a couple of shortcodes, being the only difference, their name. We don't see in the documentation, so we were wondering if its possible to provide an array of possible names or pipe between them?

like so

$facade->addHandler('hello|world', function(ShortcodeInterface $s) {
    ....
});

or

$facade->addHandler(['hello','world'], function(ShortcodeInterface $s) {
    ....
});

This would considerably shrink the codebase.

Best regards

Strip out <p> elements

For example TinyMCE, wraps everything in

tags. Block elements like embedded elements, should be stripped of this tag, for HTML5 compliance.
So if the block-element is <p>[embedcode]</p>, the resulting output should be just the embedcode, without the <p> elements.

Fallback Shortcode

Hi, I'm creating shortcode for my project. It useful when we can have default shortcode. Imagine when we use WordPress, we want to retrieve any post field (included post meta value which can be any string). It's awesome when we can do something like this:

$handler->add('*', function (ShortcodeInterface $s) {
     $shortcode_name = $s->getName();
     
      return get_post_meta(get_the_ID(), $shortcode_name, true);
});

So if we have a post with a field country and value is Hong Kong, when we run [country] shortcode, it returns Hong Kong.

facade process not working

Using Laravel 5.4

public function getContentAttribute()
{
        $facade = new ShortCodeFacade();
        $facade->addHandler('hello', function (ShortcodeInterface $s) {
            return sprintf('%s', $s->getParameter('alt_text'));
        });
        
        return $facade->process($this->post_content);
}
$this->post_content
// [img alt_text='' description='']https://noschool.xsrv.jp/wp/wp-content/uploads/2018/06/Simulator-Screen-Shot-iPhone-X-2018-06-08-at-21.40.37.png[/img]

It doesnt escape shortcodes, and cannot get parameter.

Shortcode "self-awareness"

This is a cool idea by @wollywombat to bring another set of helper methods to Shortcode class:

  • getPosition(): match number in currently processed text,
  • getNamePosition(): match number from shortcodes with the same name,
  • getMatchOffset(): match offset in processed text,
  • getParent(): parent shortcode when processing recursively or null if in root scope.

These methods names are only a proposition and any other ideas are welcome.

On large HTML files, RegularParser does not fire any handler.

I've been implementing this library on a project in my company, which scraps a whole local Wordpress with httrack and then modifies the files in order to fit to our needs.

To make some parts dynamic, we're implementing shortcodes (which we translate to custom PHP code). I've been testing your library without any problems, but when I gave it a definitive file, it didn't triggered any handler, and the parser pushed PHP memory consumption over 800MB.

After several tests, I've decided to try the RegexParser. It parsed the files correctly, and using a riddiculous amount of time and memory compared with the standard one.

I can understand the extra time and memory consumption (uncer certain limits), but I don't get why the parser didn't saw any of my tags, I am the only one who suffered this issue?

BTW, thank you very much for your work, your library is awesome and I'm enjoying it a lot!

regular parser throws exception with `param=0`

While debugging an issue with the Grav Shortcode UI plugin: (getgrav/grav-plugin-shortcode-ui#29) I discovered the regular parser will skip over an x param with the following format: [shortcode x=0].

[shortcode x="0"] works, as well as [shortcode x=1], but not x=0. This only seems to affect the regular parser. It works fine and as expected with regex and wordpress parsers.

I gave it a quick debug and it seems like the match() method in the regular parser is trying to match the wrong type (6 vs 5?). I dunno, it's a bit hard for me to fully debug this as I'm not super familiar with this tokenized parser.

The result is the parent shortcode is completely skipped, causing child shortcodes to not have a parent, hence the exception to be thrown. This seems like a pretty nasty issue as any shortcode with x=0 type params will just be skipped entirely.

Please provide working examples

Your documentations seems to be hard to understand,
Can you provide working examples for each of the base functions so that one can get better understanding?

Thanks

Modifying shortcode replacements via event handler?

Is it possible to use either Events::FILTER_SHORTCODES or Events::REPLACE_SHORTCODES to modify/extend the replacement string provided by the handler?

I am looking for a way to insert additional HTML (say, a </div>...<div>) around shortcodes. When determining this additional HTML, I'd need to know at lease the shortcode name. And I would like to keep this out of the handler, because it is context/situation specific.

Any pointers?

Unicode character breaks shortcode replacement

Hey,

I'm testing following assertion based on README example:

        $handlers = new HandlerContainer();
        $handlers->add('sample', function(ShortcodeInterface $s) {
           return (new JsonSerializer())->serialize($s);
           });
        $processor = new Processor(new RegexParser(), $handlers);

        $text = 'x [sample arg=val]cnt[/sample] y';
        $result = 'x {"name":"sample","args":{"arg":"val"},"content":"cnt"} y';
        assert($result === $processor->process($text));

it works fine unless I put some unicode characters inside shortcode.

In case of polish character ń it returns 'x {"name":"sample","parameters":{"arg":"val"},"content":"\u0144","bbCode":null}] y' (notice closing ]).
In case of 4 polish characters żółć it returns 'x {"name":"sample","parameters":{"arg":"val"},"content":"\u017c\u00f3\u0142\u0107","bbCode":null}ple] y' (notice ple] after shortcode replacement).

I'm using PHP Version 5.5.9-1ubuntu4.11 with Multibyte Support enabled.

Refactoring abstractions

Couple of loose thoughts:

  • "syntax" concept needs to be split into reusable data container (the actual Syntax class) to define fragments and regular expressions builder (regular and strict variant), with separate namespaces and interfaces for both,
  • Extractor class needs to be moved into its own namespace and renamed to RegexExtractor taking regular expression builder as a dependency (will need a factory or at least named constructor to be usable),
  • same thing for Parser class,
  • introduce extractors and parsers based on actual language parsers (with lexer and grammar),
  • HandlerContainer needs to be extracted from Processor with separate interface,
  • BC could be maintained through class_alias() but probably will be broken (these refactorings could take place when moving towards 1.0).

Issue with Nested shortcodes

I'm trying to process nested shortcodes but it doesn't work:

$syntaxBuilder = (new SyntaxBuilder())
                ->setOpeningTag('<')
                ->setClosingTag('>')
                ->setClosingTagMarker('/')
                ->setParameterValueSeparator('=')
                ->setParameterValueDelimiter('"')
                ->getSyntax();

$syntaxParser  = new RegexParser($syntaxBuilder);
$tagsHandler   = new HandlerContainer();
$tagsProcessor = new Processor(new RegexParser($syntaxBuilder), $tagsHandler);

$tagsHandler->add('div', function (ShortcodeInterface $s) {
    return $s->getContent();
});

echo $tagsProcessor->process("<div>TEST1 <div>TEST2</div></div>");

Result:

TEST1 TEST2</div>

Nested shortcode issue

Fails

<?php
include 'vendor/autoload.php';

use Thunder\Shortcode\ShortcodeFacade;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;

$facade = new ShortcodeFacade();
$facade->addHandler('hello', function(ShortcodeInterface $s) {
    return sprintf('Hello, %s!' . $s->getContent(), $s->getParameter('name'));
});

$text = '
    <p>Start</p>
    [hello name="Thomas"]
        [hello name="Peter"]
    [/hello]
    <p>End</p>
';
echo $facade->process($text);

Result

<p>Start</p>
Hello, Thomas!
  [hello name="Peter"]
[/hello]
<p>End</p>

Works

$text = '
    [hello name="Thomas"]
        [hello name="Peter"]
    [/hello]
';

Result

Hello, Thomas!
  Hello, Peter!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.