diff --git a/src/module/cssheader.php b/src/module/cssheader.php
new file mode 100644
index 0000000..c660168
--- /dev/null
+++ b/src/module/cssheader.php
@@ -0,0 +1,274 @@
+ Allowed bloc replacement */
+ private $bloc_wildcards = [
+ '%year%',
+ '%module_id%',
+ '%module_name%',
+ '%module_author%',
+ '%module_type%',
+ '%user_cn%',
+ '%user_name%',
+ '%user_email%',
+ '%user_url%',
+ ];
+
+ /** @var array Allowed action for header */
+ private $action_bloc = [];
+
+ /** @var string Parsed bloc */
+ private $bloc = '';
+
+ /** @var boolean Stop parsing files */
+ private $stop_scan = false;
+
+ /** @var string Settings bloc content */
+ private $bloc_content = '';
+
+ protected function init(): bool
+ {
+ $this->setProperties([
+ 'id' => 'cssheader',
+ 'name' => __('CSS header'),
+ 'description' => __('Add or remove phpdoc header bloc from css file'),
+ 'priority' => 340,
+ 'configurator' => true,
+ 'types' => ['plugin', 'theme'],
+ ]);
+
+ $this->action_bloc = [
+ __('Do nothing') => 0,
+ __('Add bloc if it does not exist') => 'create',
+ __('Add and overwrite bloc') => 'overwrite',
+ __('Overwrite bloc only if it exists') => 'replace',
+ __('Remove existing bloc header') => 'remove',
+ ];
+
+ $bloc_content = $this->getSetting('bloc_content');
+ $this->bloc_content = is_string($bloc_content) ? $bloc_content : '';
+
+ return true;
+ }
+
+ public function isConfigured(): bool
+ {
+ return !empty($this->getSetting('bloc_action'));
+ }
+
+ public function configure($url): ?string
+ {
+ if (!empty($_POST['save'])) {
+ $this->setSettings([
+ 'bloc_action' => !empty($_POST['bloc_action']) ? $_POST['bloc_action'] : '',
+ 'bloc_content' => !empty($_POST['bloc_content']) ? $_POST['bloc_content'] : '',
+ 'exclude_locales' => !empty($_POST['exclude_locales']),
+ 'exclude_templates' => !empty($_POST['exclude_templates']),
+ ]);
+ $this->redirect($url);
+ }
+
+ return '
+
' . __('This feature is experimental and not tested yet.') . '
+
+ ' . __('Action:') . ' ' .
+ form::combo('bloc_action', $this->action_bloc, $this->getSetting('bloc_action')) . '
+
+
+ ' .
+ form::checkbox('exclude_locales', 1, $this->getSetting('exclude_locales')) . ' ' .
+ __('Do not add bloc to files from "locales" and "libs" folder') .
+ '
+
+ ' .
+ form::checkbox('exclude_templates', 1, $this->getSetting('exclude_templates')) . ' ' .
+ __('Do not add bloc to files from "tpl" and "default-templates" folder') .
+ '
+
+ ' . __('Bloc content:') . '
+ ' .
+ form::textarea('bloc_content', 50, 10, html::escapeHTML($this->bloc_content)) . '
+
' .
+ sprintf(
+ __('You can use wildcards %s'),
+ '%year%, %module_id%, %module_name%, %module_author%, %module_type%, %user_cn%, %user_name%, %user_email%, %user_url%'
+ ) . ' ' . __('Do not put structural elements to the begining of lines.') . '
' .
+ '' . __('Exemple') . ' ' . self::$exemple . ' ';
+ }
+
+ public function openModule(): ?bool
+ {
+ $bloc = trim($this->bloc_content);
+
+ if (empty($bloc)) {
+ $this->setWarning(__('bloc is empty'));
+
+ return null;
+ }
+
+ $bloc = trim(str_replace("\r\n", "\n", $bloc));
+
+ try {
+ $this->bloc = (string) preg_replace_callback(
+ // use \u in bloc content for first_upper_case
+ '/(\\\u([a-z]{1}))/',
+ function ($str) {
+ return ucfirst($str[2]);
+ },
+ str_replace(
+ $this->bloc_wildcards,
+ [
+ date('Y'),
+ $this->module['id'],
+ $this->module['name'],
+ $this->module['author'],
+ $this->module['type'],
+ dcCore::app()->auth->getInfo('user_cn'),
+ dcCore::app()->auth->getinfo('user_name'),
+ dcCore::app()->auth->getInfo('user_email'),
+ dcCore::app()->auth->getInfo('user_url'),
+ ],
+ (string) $bloc
+ )
+ );
+ $this->setSuccess(__('Prepare header info'));
+
+ return null;
+ } catch (Exception $e) {
+ $this->setError(__('Failed to parse bloc'));
+
+ return null;
+ }
+ }
+
+ public function openDirectory(): ?bool
+ {
+ $skipped = $this->stop_scan;
+ $this->stop_scan = false;
+ if (!empty($this->getSetting('exclude_locales')) && preg_match('/\/(locales|libs)(\/.*?|)$/', $this->path_full)
+ || !empty($this->getSetting('exclude_templates')) && preg_match('/\/(tpl|default-templates)(\/.*?|)$/', $this->path_full)
+ ) {
+ if (!$skipped) {
+ $this->setSuccess(__('Skip directory'));
+ }
+ $this->stop_scan = true;
+ }
+
+ return null;
+ }
+
+ public function readFile(&$content): ?bool
+ {
+ if ($this->stop_scan || $this->path_extension != 'css' || $this->hasError()) {
+ return null;
+ }
+ if (empty($this->getSetting('bloc_action'))) {
+ return null;
+ }
+ $clean = $this->deleteDocBloc($content);
+ if ($this->getSetting('bloc_action') == 'remove') {
+ $content = $clean;
+
+ return null;
+ }
+ if ($content != $clean && $this->getSetting('bloc_action') == 'create') {
+ return null;
+ }
+ if ($content == $clean && $this->getSetting('bloc_action') == 'replace') {
+ return null;
+ }
+
+ $content = $this->writeDocBloc($clean);
+
+ return true;
+ }
+
+ /**
+ * Write bloc content in file content
+ *
+ * @param string $content Old content
+ * @return string New content
+ */
+ private function writeDocBloc(string $content): string
+ {
+ $res = preg_replace(
+ '/^(\/\*\*\n \*[\s|\n|\r\n]+)/',
+ "/**\n * " . str_replace("\n", "\n * ", trim($this->bloc)) . "\n */\n",
+ $content,
+ 1,
+ $count
+ );
+ if ($count && $res) {
+ $res = str_replace("\n * \n", "\n *\n", $res);
+ $this->setSuccess(__('Write new doc bloc content'));
+ }
+
+ return (string) $res;
+ }
+
+ /**
+ * Delete bloc content in file content
+ *
+ * @param string $content Old content
+ * @return string New content
+ */
+ private function deleteDocBloc(string $content): string
+ {
+ $res = preg_replace(
+ '/^(\*[\n|\r\n]{0,1}\s\*\/\*\*.*?\s\*\*\/\s\*[\n|\r\n]+)/msi',
+ '',
+ $content,
+ -1,
+ $count
+ );
+ if ($count) {
+ $this->setSuccess(__('Delete old doc bloc content'));
+ }
+
+ return (string) $res;
+ }
+}