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

Question: How to add classes on specific node ? #605

Closed
armetiz opened this issue Mar 30, 2021 · 22 comments · Fixed by #616
Closed

Question: How to add classes on specific node ? #605

armetiz opened this issue Mar 30, 2021 · 22 comments · Fixed by #616
Labels
do not close Issue which won't close due to inactivity enhancement New functionality or behavior question General questions about the project or usage
Milestone

Comments

@armetiz
Copy link

armetiz commented Mar 30, 2021

Question

I have a common markdown content, without any specific attributes.
When rendering markdown to HTML, I want to see some CSS Classes on HTML nodes.

Let's say :

  • All H1 nodes : .title-main
  • All Table nodes : .table
  • All Table Row nodes : .table .table-row

Is this possible without creating custom renderer ?

It could be awesome to be able to configure using the environment :

$config = [
  'html_classes' => [
    'h1' => '.title-main',
    'table' => '.table',
  ],
];

The use case is : I'm using a bought theme that request some css class. I don't want to change markdown original content, and I don't want to change theme.

Thank a lot for your awesome job on this package !

Regards,

@armetiz armetiz added the question General questions about the project or usage label Mar 30, 2021
@colinodell
Copy link
Member

You can create an event listener the handles the DocumentParsedEvent, iterates through the parsed nodes, and applies the custom classes as needed. See https://commonmark.thephpleague.com/1.5/customization/event-dispatcher/#example for an example of this.

In your case, that code would look something like this:

use League\CommonMark\EnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
// TODO: add other missing "use" statements here

class AddCustomCssClassesProcessor
{
    private $environment;

    public function __construct(EnvironmentInterface $environment)
    {
        $this->environment = $environment;
    }

    public function onDocumentParsed(DocumentParsedEvent $event)
    {
        $document = $event->getDocument();
        $walker = $document->walker();
        while ($event = $walker->next()) {
            $node = $event->getNode();

            // Only stop when we first encounter a node
            if (!$event->isEntering()) {
                continue;
            }

            if ($node instanceof Heading && $node->getLevel() === 1) {
                $node->data['attributes']['class'] = 'title-main';
            } elseif ($node instanceof Table) {
                $node->data['attributes']['class'] = 'table';
            }
            // etc.
        }
    }
}

The code above is untested but it should get you on the right track.

I do like the idea of creating a more-generic, core piece of functionality that works similar to your example, so I'll keep this open as a feature request for the upcoming 2.0 release :)

@colinodell colinodell added the enhancement New functionality or behavior label Mar 30, 2021
@colinodell colinodell added this to the v2.0 milestone Mar 30, 2021
@armetiz
Copy link
Author

armetiz commented Mar 31, 2021

awesome!

I walked throughout the documentation but the event-dispatcher. I didn't expect to find the solution in this section ;p

Thanks !

@colinodell colinodell reopened this Apr 8, 2021
@armetiz
Copy link
Author

armetiz commented Apr 9, 2021

Note, I'm using "league/commonmark": "dev-latest" (commit: 7299ca7).

if ($node instanceof Heading) {
  $node->data['attributes']['class'] = 'title-main';
}

I'm getting an ErrorException : Indirect modification of overloaded element of Dflydev\DotAccessData\Data has no effect.

@colinodell
Copy link
Member

colinodell commented May 8, 2021

@armetiz Would something like this for you?

$config = [
    'default_attributes' => [
        Heading::class => [
            'class' => function (Heading $node) {
                if ($node->getLevel() === 1) {
                    return 'title-main';
                } else {
                    return null;
                }
            },
        ],
        Table::class => [
            'class' => 'table',
        ],
        Paragraph::class => [
            'class' => ['text-center', 'font-comic-sans'],
        ],
        Link::class => [
            'class' => 'btn btn-link',
            'target' => '_blank',
        ],
    ],
];

I'm getting an ErrorException : Indirect modification of overloaded element of Dflydev\DotAccessData\Data has no effect.

That's probably because $node->data is no longer a plain array in the latest branch.

@armetiz
Copy link
Author

armetiz commented May 10, 2021

It's really cool ;)

@colinodell
Copy link
Member

New extension implemented in #616 for the upcoming 2.0.0 release! 🎉

@Zignature
Copy link

Zignature commented Jan 8, 2022

Pardon my ignorance but I'm way over my head in this :)

How would I go about adding CSS classes to task lists?
e.g. <ul class="task-list"> and <li class="task-list-item">

@colinodell
Copy link
Member

Follow the example on https://commonmark.thephpleague.com/2.1/extensions/default-attributes/, but use ListBlock::class and ListItem::class as your keys in the config.

@Zignature
Copy link

Thanks! 😃
I'll give it a shot!

@Zignature
Copy link

Well it works, but it works too good 😅
It adds those classes to all lists. I only need them on Task Lists...

@colinodell
Copy link
Member

Ah, in that case, you have two options:

  1. Write your own listener to look for TaskListItemMarker nodes in the AST. When you find one, apply the custom classes to its parent(s).
  2. Follow the example at the very bottom of the page, showing how you can use a function to determine if it's the right type of list. In your case, you'll want to check if any of its descendants/children contain a TaskListItemMarker.

@Zignature
Copy link

Thanks for the pointers!

@Zignature
Copy link

Zignature commented Jan 12, 2022

It took me a while but this is what I've come up with:

'default_attributes' => [
	ListBlock::class => [
		'class' => static function (ListBlock $listblock) {
			if ($listblock->firstChild()->firstChild()->firstChild() instanceof TaskListItemMarker) {
				return 'task-list';
			}

			return null;
		},
	],
	ListItem::class => [
		'class' => static function (ListItem $listitem) {
			if ($listitem->firstChild()->firstChild() instanceof TaskListItemMarker) {
				return 'task-list-item';
			}

			return null;
		},
	]
]

It works, but I have a sneaking suspicion there's a more elegant way.
Like I said, I am way over my head in this. Consider me a novice 😄

@Zignature
Copy link

Is it possible to assign multiple attributes in one go, or will I have to repeat a block with a new key and a new value?

@colinodell
Copy link
Member

You're definitely on the right track! Navigating the AST isn't the easiest. You could potentially do something like this instead:

'class' => static function (Node $node) {
    // Check every first child down the tree
    while (($node = $node->firstChild()) !== null) {
        if ($node instanceof TaskListItemMarker) {
            return 'task-list-item';
        }

        // If we've found a nested list we're probably too deep and don't want that nested list affecting the current one
        if ($node instanceof ListBlock) {
            break;
        }
    }

    return null;
}

Is it possible to assign multiple attributes in one go, or will I have to repeat a block with a new key and a new value?

That's not possible with this extension, but it's very possible to do with a custom event listener like this one: https://commonmark.thephpleague.com/2.1/customization/event-dispatcher/#example

If you have large documents or multiple attributes you want to add, using a custom event listener is almost certainly more efficient. default_attributes is just a shortcut for simpler cases :)

@Zignature
Copy link

I've been looking at that page :)

Where would such an event listener be saved? Or does it have to be written in the same page?

@colinodell
Copy link
Member

I'd recommend defining that listener as a separate class. (You could define an anonymous class inline with your other code, but that might get messy)

@Zignature
Copy link

Is there a recommended location to save class files, or is that arbitrary?

@colinodell
Copy link
Member

I'd recommend that you follow PSR-4

@Zignature
Copy link

Zignature commented Jan 13, 2022

Something like this?

 [ROOT]
   |-[vendor]
   |    |-[league]
   |    |-[username]
   |    |    |-[myclass]
   |    |    |   |-[src]
   |    |    |       |-MyClass.php
   |    |-autoload.php
   |-index.php

with namespace Username\MyClass\MyClass

@colinodell
Copy link
Member

Ideally, it would be more like this:

 [ROOT]
   |-[src]
   |    |-SomeFolder(s)
   |         |-MyClass.php
   |-[vendor]
   |    |-[league]
   |    |-autoload.php
   |-index.php

It's a great question but unfortunately beyond the scope of this library. I'd recommend doing some research on PSR-0, PSR-4, autoloading, namespaces, and common naming conventions - that should get you moving in the right direction.

@Zignature
Copy link

Zignature commented Jan 13, 2022

Ideally, it would be more like this:

 [ROOT]
   |-[src]
   |    |-SomeFolder(s)
   |         |-MyClass.php
   |-[vendor]
   |    |-[league]
   |    |-autoload.php
   |-index.php

Well that was my initial plan 😄

Would that make the namespace SomeFolder\MyClass?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
do not close Issue which won't close due to inactivity enhancement New functionality or behavior question General questions about the project or usage
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants