--- /dev/null
+<textarea id="{@$field->getPrefixedId()}" {*
+ *}name="{@$field->getPrefixedId()}" {*
+ *}{if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
+ *}{if $field->isRequired()} required{/if}{*
+ *}{if $field->isAutofocused()} autofocus{/if}{*
+ *}{if $field->isImmutable()} disabled{/if}{*
+ *}{if $field->getMinimumLength() !== null} minlength="{$field->getMinimumLength()}"{/if}{*
+ *}{if $field->getMaximumLength() !== null} maxlength="{$field->getMaximumLength()}"{/if}{*
+ *}{foreach from=$field->getFieldAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*
+*}>{$field->getValue()}</textarea>
+
+{include file='codemirror' codemirrorMode=$field->getLanguage() codemirrorSelector='#'|concat:$field->getPrefixedId()}
+
+<script data-relocate="true">
+ (() => {
+ document.getElementById('{@$field->getPrefixedId()}').parentNode.dir = 'ltr';
+ })();
+</script>
"__rowFormFieldContainer",
"__singleMediaSelectionFormField",
"__singleSelectionFormField",
+ "__sourceCodeFormField",
"__suffixFormFieldContainer",
"__tabFormContainer",
"__tabMenuFormContainer",
--- /dev/null
+<textarea id="{@$field->getPrefixedId()}" {*
+ *}name="{@$field->getPrefixedId()}" {*
+ *}{if !$field->getFieldClasses()|empty} class="{implode from=$field->getFieldClasses() item='class' glue=' '}{$class}{/implode}"{/if}{*
+ *}{if $field->isRequired()} required{/if}{*
+ *}{if $field->isAutofocused()} autofocus{/if}{*
+ *}{if $field->isImmutable()} disabled{/if}{*
+ *}{if $field->getMinimumLength() !== null} minlength="{$field->getMinimumLength()}"{/if}{*
+ *}{if $field->getMaximumLength() !== null} maxlength="{$field->getMaximumLength()}"{/if}{*
+ *}{foreach from=$field->getFieldAttributes() key='attributeName' item='attributeValue'} {$attributeName}="{$attributeValue}"{/foreach}{*
+*}>{$field->getValue()}</textarea>
+
+{include file='codemirror' codemirrorMode=$field->getLanguage() codemirrorSelector='#'|concat:$field->getPrefixedId()}
+
+<script data-relocate="true">
+ (() => {
+ document.getElementById('{@$field->getPrefixedId()}').parentNode.dir = 'ltr';
+ })();
+</script>
--- /dev/null
+<?php
+
+namespace wcf\system\form\builder\field;
+
+use wcf\system\form\builder\field\validation\FormFieldValidationError;
+
+/**
+ * Implementation of a form field to enter source code.
+ *
+ * @author Matthias Schmidt
+ * @copyright 2001-2021 WoltLab GmbH
+ * @license GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package WoltLabSuite\Core\System\Form\Builder\Field
+ * @since 5.5
+ */
+class SourceCodeFormField extends AbstractFormField implements
+ IAttributeFormField,
+ IAutoFocusFormField,
+ ICssClassFormField,
+ IImmutableFormField,
+ IMaximumLengthFormField,
+ IMinimumLengthFormField,
+ INullableFormField
+{
+ use TAttributeFormField;
+ use TAutoFocusFormField;
+ use TCssClassFormField;
+ use TImmutableFormField;
+ use TMaximumLengthFormField;
+ use TMinimumLengthFormField;
+ use TNullableFormField;
+
+ /**
+ * @var string|null
+ */
+ protected $language;
+
+ /**
+ * @inheritDoc
+ */
+ protected $templateName = '__sourceCodeFormField';
+
+ public const LANGUAGES = [
+ // Languages supported by CodeMirror itself.
+ 'apl',
+ 'asciiarmor',
+ 'asn.1',
+ 'asterisk',
+ 'brainfuck',
+ 'clike',
+ 'clojure',
+ 'cmake',
+ 'cobol',
+ 'coffeescript',
+ 'commonlisp',
+ 'crystal',
+ 'css',
+ 'cypher',
+ 'd',
+ 'dart',
+ 'diff',
+ 'django',
+ 'dockerfile',
+ 'dtd',
+ 'dylan',
+ 'ebnf',
+ 'ecl',
+ 'eiffel',
+ 'elm',
+ 'erlang',
+ 'factor',
+ 'fcl',
+ 'forth',
+ 'fortran',
+ 'gas',
+ 'gfm',
+ 'gherkin',
+ 'go',
+ 'groovy',
+ 'haml',
+ 'handlebars',
+ 'haskell',
+ 'haskell-literate',
+ 'haxe',
+ 'htmlembedded',
+ 'htmlmixed',
+ 'http',
+ 'idl',
+ 'javascript',
+ 'jinja2',
+ 'jsx',
+ 'julia',
+ 'livescript',
+ 'lua',
+ 'markdown',
+ 'mathematica',
+ 'mbox',
+ 'mirc',
+ 'mllike',
+ 'modelica',
+ 'mscgen',
+ 'mumps',
+ 'nginx',
+ 'nsis',
+ 'ntriples',
+ 'octave',
+ 'oz',
+ 'pascal',
+ 'pegjs',
+ 'perl',
+ 'php',
+ 'pig',
+ 'powershell',
+ 'properties',
+ 'protobuf',
+ 'pug',
+ 'puppet',
+ 'python',
+ 'q',
+ 'r',
+ 'rpm',
+ 'rst',
+ 'ruby',
+ 'rust',
+ 'sas',
+ 'sass',
+ 'scheme',
+ 'shell',
+ 'sieve',
+ 'slim',
+ 'smalltalk',
+ 'smarty',
+ 'solr',
+ 'soy',
+ 'sparql',
+ 'spreadsheet',
+ 'sql',
+ 'stex',
+ 'stylus',
+ 'swift',
+ 'tcl',
+ 'textile',
+ 'tiddlywiki',
+ 'tiki',
+ 'toml',
+ 'tornado',
+ 'troff',
+ 'ttcn',
+ 'ttcn-cfg',
+ 'turtle',
+ 'twig',
+ 'vb',
+ 'vbscript',
+ 'velocity',
+ 'verilog',
+ 'vhdl',
+ 'vue',
+ 'wast',
+ 'webidl',
+ 'xml',
+ 'xquery',
+ 'yacas',
+ 'yaml',
+ 'yaml-frontmatter',
+ 'z80',
+
+ // Additional language added/supported by us.
+ 'smartymixed',
+ ];
+
+ /**
+ * Returns the source code language used or `null` if no language is specified.
+ *
+ * By default, `null` is returned.
+ */
+ public function getLanguage(): ?string
+ {
+ return $this->language;
+ }
+
+ /**
+ * Sets the source code language used or unset a previously set language if `null` is given.
+ *
+ * @throws \InvalidArgumentException if given language is unsupported
+ */
+ public function language(?string $language): self
+ {
+ if (!\in_array($language, self::LANGUAGES)) {
+ throw new \InvalidArgumentException("Unsupported language '{$language}' given.");
+ }
+
+ $this->language = $language;
+
+ return $this;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function readValue()
+ {
+ if ($this->getDocument()->hasRequestData($this->getPrefixedId())) {
+ $this->value = $this->getDocument()->getRequestData($this->getPrefixedId());
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function validate()
+ {
+ if ($this->value === null || $this->value === '') {
+ if ($this->isRequired()) {
+ $this->addValidationError(new FormFieldValidationError('empty'));
+ }
+ } else {
+ $this->validateMinimumLength($this->value);
+ $this->validateMaximumLength($this->value);
+ }
+
+ parent::validate();
+ }
+}