You too can become a hero and support this Blog at GitHub Sponsors or Patreon.

More about support

What it does

The extension fs_code_snippet (see at TER ) brings a new content element to TYPO3. The "code snippet" content element is based on fluid_styled_content as discribed in this post. It lets you add code snippets of various programming languages as content elements to your TYPO3 website. The extension makes use of the awesome library Prism and looks like this in the frontend:

<html>
  <head>
    <title>Awesome Markup!</title>
  </head>
  <body class="nice-body">
    <h1>Hello World</h1>
  </body>
</html>

In the backend you have a new and simple content element (in the plugin tab) that has two fields that are worth mentioning:

  • Programming Language: Here you can choose what programming language your snippet is written in. Despite Prism supporting all kinds of languages only a few are supported by this extension out of the box. That is mainly to keep the included JavaScript at a reasonable size while supporting all main languages from the TYPO3 world. However, it is absolutely possible to enable support of further languages. Have a look at the example below if you are interested.
  • Code Snippet: The actual code snippet. It makes use of the T3Editor that only supports XML/HTML, CSS, JavaScript, some SQL and TypoScript syntax. But whether it supports suitable syntax highlighting or not, it provides a nice way of writing code in the backend. It has line numbers and you can use the tab key for indention. Yeah!

One neat thing about it is whenever you switch the programming language the form gets reloaded to load the fitting tokenizer and syntax scripts. Make sure to save your form before you switch, to not risk to loose any code snippet content.

Support for the following languages is enabled by default if you just install the extension from TER:

  • Apache Configuration
  • Bash
  • CSS
  • HTML
  • JavaScript
  • JSON
  • PHP
  • Typoscript
  • XML

Additionally the command-line syntax is supported, allowing a prompt and output, looking like this in the frontend:

gulp build
[16:08:16] Using gulpfile ~/path/to/web/typo3conf/ext/fs_code_snippet/gulpfile.js
[16:08:16] Starting 'build'...
[16:08:16] Starting 'build-js'...
[16:08:16] Starting 'build-css'...
[16:08:16] Finished 'build' after 25 ms
[16:08:16] Finished 'build-js' after 34 ms
[16:08:16] Finished 'build-css' after 15 ms

The fields user, host, promt and the information what lines are output instead of input are stored in a FlexForm that is only visible if "command-line" is selected from the programming language dropdown.

Top

UPDATE - TypoScript Highlighting

Inspired by and based on the post Prism.js: Typoscript Highlighting by André Knieriem I added a TypoScript highlighting to fs_code_snippet 1.4.0. It looks like this (I shortened all the keywords and tags, full version is on github):

Prism.languages.typoscript = Prism.languages.extend('javascript', {
    comment:/(\s|^)([#]{1}[^#^\r^\n]{2,}?(\r?\n|$))|((\s|^)(\/\/).*(\r?\n|$))/g,
    keyword:/\b(TEXT|CASE|COA|COA_INT|GIFBUILDER|...)\b/g,
    tag:/\b(admPanel|alt_print|auth|browser|cache|...)\b/g,
    string: [
        {
            pattern: /^([^=]*=[< ]?)((?!\]\n).)*/g,
            lookbehind: true,
            inside: {
                variable: /(\{\$.*\})|(\{(register|field|cObj):.*\})|((TSFE|file):.*\n?\s?)/g,
                keyword: /\b(TEXT|CASE|COA|COA_INT|GIFBUILDER|...)\b/g,
            }
        }
    ]
});

Let me pick some TS gems from css_styled_content to show you the result:

# ***************************************************************************
# Notice: "styles." (and "temp.") objects are UNSET after template parsing!
# Use "lib." for persisting storage of objects.
# ***************************************************************************

# Clear out any constants in this reserved room!
styles.content >

# get content
styles.content.get = CONTENT
styles.content.get {
  table = tt_content
  select.orderBy = sorting
  select.where = colPos=0
}

#**********************************
# tt_content is started
#**********************************
tt_content >
tt_content = CASE
tt_content.key.field = CType
tt_content.stdWrap {
  innerWrap.cObject = CASE
  innerWrap.cObject {
    key.field = section_frame

    default = COA
    default {
      10 = TEXT
      10 {
        cObject = CASE
        cObject {
          key.field = CType

          default = TEXT
          default {
            value = <div id="c{field:uid}"
          }

          div = TEXT
          div {
            value = <div
          }

          menu < .default
          menu {
            override = <nav id="c{field:uid}"
            override {
              if {
                value = html5
                equals.data = TSFE:config|config|doctype
              }
            }
          }
        }
        insertData = 1
      }

      20 = COA
      20 {
        # Create default class for content
        10 = TEXT
        10 {
          value = csc-default
          required = 1
          noTrimWrap = || |
        }
        # Create class for space before content
        20 = USER
        20 {
          userFunc = TYPO3\CMS\CssStyledContent\Controller\CssStyledContentController->renderSpace
          space = before
          constant = {$content.spaceBefore}
          classStdWrap {
            required = 1
            noTrimWrap = |csc-space-before-| |
          }
        }
        # Create class for space after content
        30 = USER
        30 {
          userFunc = TYPO3\CMS\CssStyledContent\Controller\CssStyledContentController->renderSpace
          space = after
          constant = {$content.spaceAfter}
          classStdWrap {
            required = 1
            noTrimWrap = |csc-space-after-| |
          }
        }
        stdWrap {
          trim = 1
          noTrimWrap = | class="|"|
          required = 1
        }
      }

      30 = TEXT
      30 {
        cObject = CASE
        cObject {
          key.field = CType

          default = TEXT
          default {
            value = >|</div>
          }

          menu < .default
          menu {
            override = >|</nav>
            override {
              if {
                value = html5
                equals.data = TSFE:config|config|doctype
              }
            }
          }
        }
      }
    }

Please let me know what is missing or can be improved here, so one day we can open a merge request to add typoscript to the supported languages of prism.js.

Top

How it works

It's pretty simple. We register our new CType as desribed here and then configure our TCA like this:

call_user_func(function () {

    $frontendLanguageFilePrefix = 'LLL:EXT:frontend/Resources/Private/Language/locallang_ttc.xlf:';

    // Add the CType "fs_code_snippet"
    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTcaSelectItem(
        'tt_content',
        'CType',
        [
            'Code Snippet',
            'fs_code_snippet',
            'fs-code-snippet'
        ],
        'list',
        'after'
    );

    // Add the column programming_language too tt_content
    $newColumn = [
        'programming_language' => [
            'exclude' => true,
            'label' => 'Programming Language',
            'config' => [
                'type' => 'select',
                'renderType' => 'selectSingle',
                'items' => [
                    ['None', \TYPO3\CMS\T3editor\Form\Element\T3editorElement::MODE_MIXED],
                    ['Apache Config', \DanielGoerz\FsCodeSnippet\Enumeration\CodeSnippetLanguage::APACHE_CONFIGURATION],
                    ['Bash', \DanielGoerz\FsCodeSnippet\Enumeration\CodeSnippetLanguage::BASH],
                    ['Command-line', \DanielGoerz\FsCodeSnippet\Enumeration\CodeSnippetLanguage::COMMANDLINE],
                    ['CSS', \TYPO3\CMS\T3editor\Form\Element\T3editorElement::MODE_CSS],
                    ['HTML', \TYPO3\CMS\T3editor\Form\Element\T3editorElement::MODE_HTML],
                    ['JavaScript', \TYPO3\CMS\T3editor\Form\Element\T3editorElement::MODE_JAVASCRIPT],
                    ['JSON', \DanielGoerz\FsCodeSnippet\Enumeration\CodeSnippetLanguage::JSON],
                    ['PHP', \TYPO3\CMS\T3editor\Form\Element\T3editorElement::MODE_PHP],
                    ['Typoscript', \TYPO3\CMS\T3editor\Form\Element\T3editorElement::MODE_TYPOSCRIPT],
                    ['XML', \TYPO3\CMS\T3editor\Form\Element\T3editorElement::MODE_XML]
                ],
                'default' => \TYPO3\CMS\T3editor\Form\Element\T3editorElement::MODE_MIXED
            ]
        ]
    ];
    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addTCAcolumns('tt_content', $newColumn);

    // Reload on change
    $GLOBALS['TCA']['tt_content']['ctrl']['requestUpdate'] .= ',programming_language';
    // Use type icon
    $GLOBALS['TCA']['tt_content']['ctrl']['typeicon_classes']['fs_code_snippet'] = 'fs-code-snippet';
    // What fields should be displayed
    $GLOBALS['TCA']['tt_content']['types']['fs_code_snippet'] = [
        'showitem' => '
            --palette--;' . $frontendLanguageFilePrefix . 'palette.general;general,
            --palette--;' . $frontendLanguageFilePrefix . 'palette.header;header,
            programming_language,pi_flexform,
            bodytext,
            --div--;' . $frontendLanguageFilePrefix . 'tabs.access,
                hidden;' . $frontendLanguageFilePrefix . 'field.default.hidden,
                --palette--;' . $frontendLanguageFilePrefix . 'palette.access;access,
        ',
    ];

    // Overwrite behavior of bodytext for fs_code_snippet
    $GLOBALS['TCA']['tt_content']['types']['fs_code_snippet']['columnsOverrides'] = [
        'bodytext' => [
            'label' => 'Code Snippet',
            'config' => [
                'type' => 'text',
                'renderType' => 'fs_code_snippet',
                'format' => 'html',
                'rows' => 20,
            ],
        ],
        'pi_flexform' => [
            'label' => 'Command-line options',
            'displayCond' => 'FIELD:programming_language:=:' . \DanielGoerz\FsCodeSnippet\Enumeration\CodeSnippetLanguage::COMMANDLINE
        ]
    ];

    // Add a flexform to the fs_code_snippet CType
    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addPiFlexFormValue(
        '',
        'FILE:EXT:fs_code_snippet/Configuration/FlexForms/fs_code_snippet_flexform.xml',
        'fs_code_snippet'
    );

    \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::addStaticFile('fs_code_snippet', 'Configuration/TypoScript', 'Code Snippet CE');
});

We basically configure two fields here: programming_language as a select from the constants of the CodeSnippetLanguage enumeration class (check out TYPO3s implementation of the enumeration type ). Noteworthy is the addition of the field to the requestUpdate fields of tt_content. This ensures the form is reloaded if programming_language is changed, so the fitting tokenizer can be loaded.

To make this possible the bodytext field is overwritten for our type to include a custom render type 'renderType' => 'fs_code_snippet'. This render type is registered in our ext_localconf.php like this:

$GLOBALS['TYPO3_CONF_VARS']['SYS']['formEngine']['nodeRegistry'][1449747562] = array(
    'nodeName' => $extKey,
    'priority' => 50,
    'class' => \DanielGoerz\FsCodeSnippet\Form\Element\CodeSnippetElement::class,
);

The array key within 'nodeRegistry' is a timestamp while $extKey just holds the name of the extension, matching the renderType => 'fs_code_snippet' from the TCA. The implementation is only a minor extension of the T3Editor element which is more forgiveable regarding unsupported modes in order to keep the extension extendible itself (see below):

namespace DanielGoerz\FsCodeSnippet\Form\Element;

use DanielGoerz\FsCodeSnippet\Enumeration\CodeSnippetLanguage;
use TYPO3\CMS\T3editor\Form\Element\T3editorElement;

class CodeSnippetElement extends T3editorElement
{
     /**
     * Render t3editor element
     *
     * @return array As defined in initializeResultArray() of AbstractNode
     */
    public function render()
    {
        $this->allowSupportedLanguages();
        if (!empty($this->data['databaseRow']['programming_language'][0])) {
            $this->data['parameterArray']['fieldConf']['config']['format'] = $this->data['databaseRow']['programming_language'][0];
        }
        try {
            return parent::render();
        } catch (\InvalidArgumentException $e) {
            // Format not allowed internally
            $this->data['parameterArray']['fieldConf']['config']['format'] = parent::MODE_MIXED;
        }
        return parent::render();
    }

    /**
     * Determine the correct parser js file for given mode
     *
     * @param string $mode
     * @return string Parser file name
     */
    protected function getParserfileByMode($mode)
    {
        if ($mode === parent::MODE_PHP) {
            return json_encode(['../contrib/php/js/tokenizephp.js', '../contrib/php/js/parsephp.js']);
        }
        $parserfile = parent::getParserfileByMode($mode);
        if ($parserfile === '[]') {
            return parent::getParserfileByMode(parent::MODE_MIXED);
        }
        return $parserfile;
    }

    /**
     * @return void
     */
    protected function allowSupportedLanguages()
    {
        $supportedLanguages = CodeSnippetLanguage::getConstants();
        foreach ($supportedLanguages as $supportedLanguage) {
            $this->allowedModes[] = $supportedLanguage;
        }
    }
}

The most interesting part ist the render() method, that maps the content of the programming_language field dynamically to the mode of the T3Editor and if it is an unsupported mode falls back to the mixed mode. Additionally all supported languages are added. They are stored in an enumeration as mentioned above.

The last piece in the puzzle is the DataProvider that maps the T3Editor constants used on the TYPO3 side to the strings that Prism expects in the markup to overlay the syntax highlightning:

namespace DanielGoerz\FsCodeSnippet\DataProcessing;

use DanielGoerz\FsCodeSnippet\Enumeration\CodeSnippetLanguage;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\FlexFormService;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Frontend\ContentObject\DataProcessorInterface;
use TYPO3\CMS\Frontend\ContentObject\Exception\ContentRenderingException;
use TYPO3\CMS\T3editor\Form\Element\T3editorElement;

class CodeSnippetProcessor implements DataProcessorInterface
{
    /**
     * Process data for the CType "fs_code_snippet"
     *
     * @param ContentObjectRenderer $cObj The content object renderer, which contains data of the content element
     * @param array $contentObjectConfiguration The configuration of Content Object
     * @param array $processorConfiguration The configuration of this processor
     * @param array $processedData Key/value store of processed data (e.g. to be passed to a Fluid View)
     * @return array the processed data as key/value store
     * @throws ContentRenderingException
     */
    public function process(ContentObjectRenderer $cObj, array $contentObjectConfiguration, array $processorConfiguration, array $processedData)
    {
        // Map the T3editorElement constants to the string expected by prism
        switch ($processedData['data']['programming_language']) {
            case T3editorElement::MODE_HTML:
            case T3editorElement::MODE_XML:
                $programmingLanguage = 'markup';
                break;
            case T3editorElement::MODE_TYPOSCRIPT:
                $programmingLanguage = 'none';
                break;
            case CodeSnippetLanguage::COMMANDLINE:
                $programmingLanguage = CodeSnippetLanguage::BASH;
                $flexFormContent = $this->getFlexFormContentAsArray($processedData['data']['pi_flexform']);
                if (!empty($flexFormContent['settings']['commandline'])) {
                    $processedData['commandline'] = $flexFormContent['settings']['commandline'];
                }
                break;
            default:
                $programmingLanguage = $processedData['data']['programming_language'];
        }
        $processedData['programmingLanguage'] = $programmingLanguage;
        $processedData['data']['bodytext'] = rtrim($processedData['data']['bodytext'], "\n\r\t");
        return $processedData;
    }

    /**
     * @param string $flexFormContent
     * @return array
     */
    protected function getFlexFormContentAsArray($flexFormContent)
    {
        /** @var FlexFormService $flexFormService */
        $flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
        return $flexFormService->convertFlexFormContentToArray($flexFormContent);
    }
}

And with this it should be possible to add more programming languages to the extension.

Top

Enabling further programming languages

To do so, you have to make sure your extension is loaded after fs_code_snippet by putting it in the 'depends' array in your ext_emconf.php. Then you can add several programming languages in Configuration/TCA/Overrides/tt_content.php. You should use the keys that Prism expects because the T3Editor will fall back to mixed mode anyway. All those keys are stored in constants in an Enumeration \DanielGoerz\FsCodeSnippet\Enumeration\CodeSnippetLanguage. As an example we are going to add support for python:

$GLOBALS['TCA']['tt_content']['columns']['programming_langauge']['config']['items'][] =[
  'Python' => \DanielGoerz\FsCodeSnippet\Enumeration\CodeSnippetLanguage::PYTHON
];

That would be enough if the python component of Prism was already present in the JS file fs_code_snippet brings out of the box. Guess what? Its not! The shipped JS file contains only the needed parts of the Prism library as it is build out of many components (e.g. one for every programming language) and we only use a few of them by default as you already know by now. Thus you have to include the python component in your JavaScript.

You can either download it from prismjs.com and include it or use the gulp tasks of fs_code_snippet to rebuild the JS file with additional components. To do so, you should install the node modules and download the Prism library as a bower component. Go to the extension folder and run

npm install
bower install

Now you can edit the gulpfile.js and add as many Prism components as you need. Save the file if you are done and run

gulp build

This would work best if you copy the bower.json, .bowerrc, package.json and the gulpfile.js to your own extension and execute the build there. Then just overwrite the include of the JS file of fs_code_snippet with your own file and you should have python support in the frontend.

Top