picocms / Pico

Pico is a stupidly simple, blazing fast, flat file CMS.
http://picocms.org/
MIT License
3.83k stars 614 forks source link

How to make img relative path work? #493

Closed greencopper closed 5 years ago

greencopper commented 5 years ago

Hi,

We're using picocms at our office on our intranet. People are using editors such as Retext to write the Markdown and we really need to enable relative paths so that Retext and other editors displays images correctly. We cannot use absolute paths.

I am considering hacking picocms, but since I can see this feature has been requested several times, I think it's much better to enable this using a setting in the configuration.

Before traversing the entire codebase in order to locate the relevant code I want to ask where the code dealing with paths resides and if there is an easy "hack" to make relative paths work when needed?

Kind regards.

PhrozenByte commented 5 years ago

Actually there's no code dealing with paths in Markdown files in Pico, all paths (including relative paths) are taken as-is (even %base_url% is just a placeholder that is replaced by Pico's base URL before parsing Markdown). So, if you use a link like ![Cats](assets/cats.jpg), Pico will parse this to <img src="assets/cats.jpg" alt="Cats" />. After adding a appropriate base HTML attribute (e.g. <base href="{{ base_url }}" />) to the <head> of your Twig templates (e.g. themes/my_theme/index.twig), relative paths like the one shown above work just fine.

The problem is that ReText doesn't know anything about this and interprets paths relative to the current file. So, for example, if there's a ![Cats](assets/cats.jpg) in content/sub/page.md, what file is supposed to be included? It should be assets/cats.jpg, however, since ReText treats all paths relative to the current file, it thinks the path is content/sub/assets/cats.jpg. This won't work for obvious reasons, in ReText you'd need a path like ../../assets/cats.jpg, resulting in content/sub/../../assets/cats.jpg (equals assets/cats.jpg).

You could write a simple Parsedown extension (hook into Parsedown::inlineLink()) to change relative URLs like ../../assets/cats.jpg to %base_url%/assets/cats.jpg.

Try something like the following (untested code):

class ParsedownRelativePicoPlugin extends AbstractPicoPlugin
{
    public function onParsedownRegistered(Parsedown &$parsedown)
    {
        $config = $this->getPico()->getConfig();

        $parsedown = new ParsedownRelative();
        $parsedown->setPico($this->getPico());
        $parsedown->setBreaksEnabled((bool) $config['content_config']['breaks']);
        $parsedown->setMarkupEscaped((bool) $config['content_config']['escape']);
        $parsedown->setUrlsLinked((bool) $config['content_config']['auto_urls']);
    }
}

class ParsedownRelative extends ParsedownExtra
{
    protected $pico;

    public function setPico(Pico $pico)
    {
        $this->pico = $pico;
    }

    public function getPico()
    {
        return $this->pico;
    }

    protected function inlineLink($excerpt)
    {
        $link = parent::inlineLink($excerpt);
        if ($link === null) {
            return;
        }

        $href = $link['element']['attributes']['href'];
        if (($href[0] !== '/') && !preg_match('#^[A-Za-z][A-Za-z0-9+\-.]*://#', $href)) {
            $basePath = dirname($this->getPico()->getRequestFile());
            $targetPath = $this->normalizePath($basePath . '/' . $href);

            $rootDir = $this->getPico()->getRootDir();
            $rootDirLength = strlen($rootDir);
            if (substr($targetPath, 0, $rootDirLength) === $rootDir) {
                $link['element']['attributes']['href'] = $this->getPico()->getBaseUrl() . substr($targetPath, $rootDirLength);
            }
        }

        return $link;
    }

    protected function normalizePath($path)
    {
        $absolutePath = '';
        if (DIRECTORY_SEPARATOR === '\\') {
            if (preg_match('/^(?>[a-zA-Z]:\\\\|\\\\\\\\)/', $path, $pathMatches) === 1) {
                $absolutePath = $pathMatches[0];
                $path = substr($path, strlen($absolutePath));
            }
        } else {
            if ($path[0] === '/') {
                $absolutePath = '/';
                $path = substr($path, 1);
            }
        }

        $path = str_replace('\\', '/', $path);
        $pathParts = explode('/', $path);

        $resultParts = array();
        foreach ($pathParts as $pathPart) {
            if (($pathPart === '') || ($pathPart === '.')) {
                continue;
            } elseif ($pathPart === '..') {
                array_pop($resultParts);
                continue;
            }
            $resultParts[] = $pathPart;
        }

        return $absolutePath . implode('/', $resultParts);
    }
}
stale[bot] commented 5 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed in two days if no further activity occurs. Thank you for your contributions! :+1:

stale[bot] commented 5 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed in two days if no further activity occurs. Thank you for your contributions! :+1: