Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Markdown to Shell #1029

Open
Renji-FR opened this issue Jul 10, 2024 · 2 comments
Open

Markdown to Shell #1029

Renji-FR opened this issue Jul 10, 2024 · 2 comments
Labels
enhancement New functionality or behavior

Comments

@Renji-FR
Copy link

Description

For CLI application using PHP CLI, the goal is to render Markdown to Shell output

We must use the Shell statements like this one:

echo "\033[92mMy text\033[0m"

I already have code that work for my project/application.

I used this schema in order to custom some renderer:

            $builder->addSchema('commonmark', Expect::structure([
                'use_asterisk' => Expect::bool(true),
                'use_underscore' => Expect::bool(true),
                'enable_strong' => Expect::bool(true),
                'enable_em' => Expect::bool(true),
                'unordered_list_markers' => Expect::listOf('string')->min(1)->default(['*', '+', '-'])->mergeDefaults(false),

                'renderer' => Expect::structure([
                    'heading_line_starter' => Expect::string(""),
                    'quoted_line_starter' => Expect::string(""),
                    'ordered_list_bullet' => Expect::string("%d. "),
                    'unordered_list_bullet' => Expect::string(""),
                ]),
            ]));

Example

Let me know if you want to implement this feature in your project.

I would like to create a pull request / merge request if this feature is useful for your project.

These are the classes that I created to implement this feature:

  • src/Core/Markdown/Extension/ShellCoreExtension.php
  • src/Core/Markdown/Renderer/Block/BlockQuoteRenderer.php
  • src/Core/Markdown/Renderer/Block/DocumentRenderer.php
  • src/Core/Markdown/Renderer/Block/FencedCodeRenderer.php
  • src/Core/Markdown/Renderer/Block/HeadingRenderer.php
  • src/Core/Markdown/Renderer/Block/IndentedCodeRenderer.php
  • src/Core/Markdown/Renderer/Block/ListBlockRenderer.php
  • src/Core/Markdown/Renderer/Block/ListItemRenderer.php
  • src/Core/Markdown/Renderer/Block/ParagraphRenderer.php
  • src/Core/Markdown/Renderer/Block/ThematicBreakRenderer.php
  • src/Core/Markdown/Renderer/Inline/CodeRenderer.php
  • src/Core/Markdown/Renderer/Inline/EmphasisRenderer.php
  • src/Core/Markdown/Renderer/Inline/LinkRenderer.php
  • src/Core/Markdown/Renderer/Inline/NewlineRenderer.php
  • src/Core/Markdown/Renderer/Inline/StrongRenderer.php
  • src/Core/Markdown/Renderer/Inline/TextRenderer.php
  • src/Core/Markdown/Renderer/ShellRenderer.php
  • src/Core/Markdown/ShellConverter.php
  • src/Core/Markdown/Util/Shell.php
  • src/Core/Markdown/Util/ShellElement.php

I already created my unit tests too.

Did this project help you today? Did it make you happy in any way?

Yes, I created all classes allowing to render Markdown to Shell output.

@Renji-FR Renji-FR added the enhancement New functionality or behavior label Jul 10, 2024
@Renji-FR
Copy link
Author

Renji-FR commented Jul 10, 2024

Just for the issue, this is my class ShellElement

<?php
    declare(strict_types=1);
    
    namespace Core\Markdown\Util;

    final class ShellElement implements \Stringable
    {
        /**
         * @var array<string, string|bool>
         */
        private array $attributes = [];

        /**
         * @var \Stringable|\Stringable[]|string
         */
        private mixed $contents;

        /**
         * @psalm-readonly
         */
        private bool $resetClosing;

        /**
         * @param array<string, string|string[]|bool>   $attributes   Array of attributes (values should be unescaped)
         * @param \Stringable|\Stringable[]|string|null $contents     Inner contents, pre-escaped if needed
         * @param bool                                  $resetClosing Append reset statement at the end
         */
        public function __construct(
            array $attributes = [],
            mixed $contents = '',
            bool $resetClosing = false
        ) {
            $this->resetClosing = $resetClosing;

            foreach ($attributes as $name => $value) {
                $this->setAttribute($name, $value);
            }

            $this->setContents($contents ?? '');
        }

        /**
         * @return array<string, string|bool>
         *
         * @psalm-immutable
         */
        public function getAllAttributes(): array
        {
            return $this->attributes;
        }

        /**
         * @return string|bool|null
         *
         * @psalm-immutable
         */
        public function getAttribute(string $key): mixed
        {
            return $this->attributes[$key] ?? null;
        }

        /**
         * @param string|string[]|bool $value
         */
        public function setAttribute(string $key, mixed $value = true): self
        {
            if (\is_array($value)) {
                $this->attributes[$key] = \array_unique($value);
            } else {
                $this->attributes[$key] = $value;
            }

            return $this;
        }

        /**
         * @return \Stringable|\Stringable[]|string
         *
         * @psalm-immutable
         */
        public function getContents(bool $asString = true): mixed
        {
            if (!$asString) {
                return $this->contents;
            }

            return $this->_getContentsAsString();
        }

        /**
         * Sets the inner contents of the tag (must be pre-escaped if needed)
         *
         * @param \Stringable|\Stringable[]|string $contents
         */
        public function setContents(mixed $contents): self
        {
            $this->contents = $contents ?? ''; // @phpstan-ignore-line

            return $this;
        }

        /**
         * @psalm-immutable
         */
        private function _getContentsAsString(): string
        {
            if (\is_string($this->contents)) {
                return $this->contents;
            }
            elseif (\is_array($this->contents)) {
                return \implode('', $this->contents);
            }
            else {
                return (string) $this->contents;
            }
        }

        /**
         * @psalm-immutable
         */
        public function __toString(): string
        {
            if ($this->contents === '') {
                return '';
            }

            $result = '';

            $styles = $this->attributes['styles'] ?? [];

            if (is_array($styles) && ($style = implode(';', $styles)) !== '') {
                $result .= "\033[" . $style . "m";
            }

            $result .= $this->_getContentsAsString();

            if ($this->resetClosing) {
                $result .= "\033[0m";
            }
            else
            {
                $resetStyles = [];

                foreach ($styles as $style)
                {
                    if ($style >= 1 && $style <= 9) {
                        $resetStyles[] = $style + 20;
                    }
                    elseif ($style >= 30 && $style < 39 || $style >= 90 && $style <= 99) {
                        $resetStyles[] = 39;
                    }
                    elseif ($style >= 40 && $style < 49 || $style >= 100 && $style <= 109) {
                        $resetStyles[] = 49;
                    }
                }

                if (($resetStyle = implode(';', $resetStyles)) !== '') {
                    $result .= "\033[" . $resetStyle . "m";
                }
            }

            return $result;
        }
    }

@Renji-FR
Copy link
Author

Renji-FR commented Jul 12, 2024

Another solution is to use the Termwind project:

<?php
    declare(strict_types=1);

    namespace PhpCliShell\Core\Markdown;

    use League\CommonMark\CommonMarkConverter;
    use League\CommonMark\Environment\Environment;
    use League\CommonMark\Exception\CommonMarkException;

    use Termwind\HtmlRenderer;

    /**
     * Converts CommonMark-compatible Markdown to Shell output.
     */
    final class HtmlConverter
    {
        /**
         * @psalm-readonly
         */
        private CommonMarkConverter $converter;


        /**
         * Create a new Markdown converter pre-configured for CommonMark
         *
         * @param array<string, mixed> $config
         */
        public function __construct(array $config = [])
        {
            $this->converter = new CommonMarkConverter($config);
        }

        public function getEnvironment(): Environment
        {
            return $this->converter->getEnvironment();
        }

        /**
         * Converts Markdown to Shell output.
         *
         * @param string $input Markdown input
         * @return string Rendered Shell output
         */
        public function convert(string $input): string
        {
            $htmlContent = $this->converter->convert($input);
            return (new HtmlRenderer)->parse((string) $htmlContent)->toString();
        }

        /**
         * Converts CommonMark to Shell output.
         *
         * @see HtmlConverter::convert()
         *
         * @throws CommonMarkException
         */
        public function __invoke(string $markdown): string
        {
            return $this->convert($markdown);
        }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New functionality or behavior
Projects
None yet
Development

No branches or pull requests

1 participant