Closed lxqueen closed 6 years ago
Yes, the simplest way to do this would be to use .blade.md
files. You can clean it up a little bit by adding a global helper function to config.php
so you don't have to handle the link concatenation in your templates. Something like:
return [
'baseUrl' => 'https://example.com/wiki',
'link' => function ($page, $link) {
return $page->baseUrl . '/' . trim($link, '/');
},
];
which would clean up your templates a tiny bit: [link text]({{ $page->link('link') }})
.
I think that would be much easier than running through each of your files in an afterBuild
event listener.
You could extend the markdown parser and add a custom rule (see this PR for an example on how to do that), but bear in mind that you wouldn't have access to any environment-specific config values (if you put a baseUrl
in config.production.php
, for instance). So I'd still recommend the blade.md
solution.
Hi @damiani, thank you very much! This does seem like the easiest way to do it right now, time for me to go through my wiki and sort out all the links!
Shall I leave this issue up to you and the other devs' judgment on if they want to close this or keep it open, in case there's any other input people have? I don't mind either way :smiley:
Had the same issue.
In my case, I need the user to be able to write simple, relative links in Markdown, i.e. [click here](/slug/#anchor)
instead of using Blade - mainly because I need to keep things simple for the casual user, who can use anchors and an 'offline' build script that always uses non-pretty URLs, which complicates things a lot.
I just extended the Markdown parser and overrode the inlineLink
method, where I get the Jigsaw config through dependency injection from another class, check the environment, and massage the relative link accordingly (i.e. for pretty URLs it outputs ../slug/#anchor
and for non-pretty, ../slug.html#anchor
. Also doing other stuff like adding target="_blank"
and rel="noopener"
to external URLs, as well as custom classes.
If you're thinking of going that route, do keep in mind that the inlineLink
method applies to URLs in both text links and image markdown, so you're going to have to check if it's an image path...
On this note, it would be useful if Jigsaw used Parsedown Extra. Mni\FrontYAML\Markdown\MarkdownParser
's interface would allow for a bridge, but I guess this is a topic for a separate issue :)
@hellocosmin Would you be willing to share some of your code here so that @lxqueen (and others who encounter this need) have some guidance if they want to go the inlineLink
route?
And I agree about using Parsedown Extra
. I'm going to do some testing first to make sure it doesn't break anything, before switching it to be the default parser.
Sure thing @damiani, and big thanks for looking into Parsedown Extra! Have a few things on my plate right now, but will post the code today.
Right, so I initially started from this article, which explains how to add build-time syntax highlighting. You basically need to override the default Parsedown parser with a custom implementation.
Although the article explains autoload and such, I'll go through each step:
We'll be working under the App
namespace, so we need to autoload it. Add a composer.json
in the project root:
{
"require": {
"tightenco/jigsaw": "^1.0"
},
"autoload": {
"psr-4": {
"App\\": "app/"
}
}
}
We need a way to get the config outside of the build process. What I'm doing is to dump the config in a JSON file, which I can later read and retrieve settings from. Thankfully, Jigsaw now provides 'events', which makes this very easy.
JigsawConfig.php
in app/listeners/
add this to it:
<?php
namespace App\Listeners;
use TightenCo\Jigsaw\Jigsaw;
class JigsawConfig
{
public function handle(Jigsaw $jigsaw)
{
$jigsaw->writeSourceFile('_assets/data/config.json', $jigsaw->getConfig());
}
}
finally, we need to register an event that will load our class and trigger its handle()
method. We do that in bootstrap.php
:
<?php
use App\Listeners\JigsawConfig;
/** @var $container \Illuminate\Container\Container */
/** @var $events \TightenCo\Jigsaw\Events\EventBus */
/** @var $jigsaw \TightenCo\Jigsaw\Jigsaw */
$events->beforeBuild(JigsawConfig::class);
I'm saving the config in the _assets
folder, so that it doesn't automatically get copied over at build time.
We'll also need a way to get that config we just saved. Totally up to you - I just created another class with a get()
method on it:
// app/GetJigsawConfig.php
<?php
namespace App;
class GetJigsawConfig {
public function get()
{
if (file_exists('source/_assets/data/config.json')) {
return json_decode(file_get_contents('source/_assets/data/config.json'));
}
return false;
}
}
Now comes the fun part. We need to create a class that implements the default Parsedown parser used by Jigsaw, and provide it with our own custom implementation (where we can extend Parsedown and basically override its functions).
Create app/ParsedownParser.php
:
// app/ParsedownParser.php
<?php
namespace App;
use Mni\FrontYAML\Markdown\MarkdownParser;
use App\CustomParsedown;
class ParsedownParser implements MarkdownParser
{
/**
* ParsedownParser constructor.
*/
public function __construct()
{
$this->parser = new CustomParsedown();
}
/**
* @param string $markdown
* @return string
*/
public function parse($markdown)
{
return $this->parser->text($markdown);
}
}
App\CustomParsedown
will be our custom Parsedown implementation, let's create it:
// app/CustomParsedown.php
<?php
namespace App;
use App\GetJigsawConfig;
use Parsedown as BaseParsedown;
class CustomParsedown extends BaseParsedown
{
protected $config;
public function __construct()
{
$jigsawConfig = new GetJigsawConfig();
$this->config = $jigsawConfig->get();
}
}
As you can see, our class is extending Parsedown - this allows us to manipulate Parsedown's output by overriding its methods.
What we're doing here (for now) is getting the Jigsaw config we saved earlier through dependency injection, so that we can access config keys like $this->config->pretty
or $this->config->baseUrl
.
Parsedown has an example of doing this. For our links, we'll need to override the inlineLink()
method.
Caveat: the URL/path you provide when writing something like [link](some-path/)
is used by Parsedown for both links and image src's (think ![alt text](/path/to/image.jpg)
). So we need to make sure we're not applying our transformations to images - and that's what the $ext
variable is for.
The following is not very handsome, I agree - I just made it work, so don't hate :)
We will be doing the following:
target="_blank"
and rel="noopener"
to external links[jump](#where-to)
)change the href=""
of an <a>
, depending on pretty
in the config.
Warning: this last one is highly specific to my setup. It requires that you write links in markdown with a /
separator (for example: [click me](otherpage/)
or [click me](otherpage/#some-id)
). It could be better, it also might not suit your needs, you'll mostly likely need to change it...
OK, enough talk, change app/CustomParsedown.php
to look like this:
// app/CustomParsedown.php
<?php
namespace App;
use App\GetJigsawConfig;
use Parsedown as BaseParsedown;
class CustomParsedown extends BaseParsedown
{
protected $config;
public function __construct()
{
$jigsawConfig = new GetJigsawConfig();
$this->config = $jigsawConfig->get();
}
/**
* Extra link handling
* @param array $Excerpt
* @return array
*/
protected function inlineLink($Excerpt)
{
$Link = parent::inlineLink($Excerpt);
if (! isset($Link)) {
return null;
}
$isAnchorLink = false;
$href = $Link['element']['attributes']['href'];
$ext = strtolower(pathinfo($href, PATHINFO_EXTENSION));
$isImage = in_array($ext, ['gif', 'jpg', 'jpeg', 'png', 'svg']);
// 1. Add target and rel to external links
if ($this->isExternalUrl($href) && ! $isImage) {
$Link['element']['attributes']['target'] = '_blank';
$Link['element']['attributes']['rel'] = 'noopener';
}
else {
// 2. Add scroll-to class to anchor links
if (preg_match('/#(.+)/', $href, $matches)) {
$Link['element']['attributes']['class'] = 'scroll-to';
$isAnchorLink = true;
}
// 3. Correct relative paths based on build environment
if (! $isImage && $this->config) {
if (! $this->config->pretty) {
$Link['element']['attributes']['href'] = str_replace('/', '.html', ltrim($href, '/'));
} else {
// Set href depending on whether it's a link to some-other-page/#anchor or just an in-page #anchor
if (preg_match('/^(.+)?(?=#)/', $href, $hits)) {
$href = $hits[0] ? '../' . $href : $href;
}
$Link['element']['attributes']['href'] = $href;
}
}
}
return $Link;
}
/**
* Check if a URL is internal or external
* @param string $url
* @param null $internalHostName
* @return bool
*/
protected function isExternalUrl($url, $internalHostName = null) {
$components = parse_url($url);
$internalHostName = !$internalHostName && isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : $internalHostName;
// we will treat url like '/relative.php' as relative
if (empty($components['host'])) {
return false;
}
// url host looks exactly like the local host
if (strcasecmp($components['host'], $internalHostName) === 0) {
return false;
}
$isNotSubdomain = strrpos(strtolower($components['host']), '.'.$internalHostName) !== strlen($components['host']) - strlen('.'.$internalHostName);
return $isNotSubdomain;
}
}
Finally, we need to update bootstrap.php
and bind our custom implementation to the container. It should look like this now:
<?php
use App\ParsedownParser;
use App\Listeners\JigsawConfig;
use Mni\FrontYAML\Markdown\MarkdownParser;
/** @var $container \Illuminate\Container\Container */
/** @var $events \TightenCo\Jigsaw\Events\EventBus */
/** @var $jigsaw \TightenCo\Jigsaw\Jigsaw */
$container->bind(MarkdownParser::class, ParsedownParser::class);
$events->beforeBuild(JigsawConfig::class);
That's all, hope I didn't forget anything.
Oh, right, one last thing: to make it easier to test the non-pretty URL handling, I added an extra script in package.json
:
"offline": "cross-env NODE_ENV=offline node_modules/webpack/bin/webpack.js --progress --hide-modules --env=offline --config=node_modules/laravel-mix/setup/webpack.config.js",
And, of course, a config.offline.php
file, where I set the pretty URLs to false
:
<?php
return [
'pretty' => false,
];
@hellocosmin Thanks so much for documenting this, it will be very helpful for others seeking to perform advanced markdown customizations!
So I'm trying to build a site, with the final production URL being at https://example.com/wiki/. The only issue I'm having is trying to link internally to pages doesn't work in Markdown, as using
[link text](/link/)
would just lead you to the domain root, resulting in https://example.com/link/ instead of https://example.com/wiki/link/ in the final version.As far as I'm aware, there are a few ways to fix this, but I don't think either is particularly ideal:
[link text](/wiki/link/)
, which is annoying for trying to run locally or if I need to move my site in any form, local or production..blade.md
and have links as[link text]({{ $page->baseUrl }}link/)
or[link text]({{ $page->baseUrl . 'link/' }})
, which while functionally works, isn't particularly pretty for readability when I'm throwing in heaven-knows-how-many links.[link text]({{ $collection->get('link')->getUrl() }})
.I was considering fixing this with the new hooks available since 1.1, and perhaps doing a find-replace on links that use a custom scheme (replacing links with something like
local://
orjigsaw://
in the Markdown with the real base URL automatically on build), but haven't really figured out how to do this.I don't know if this is really in scope or too niche of a use-case for this generator, but it was worth asking. Perhaps exposing FrontYAML/Parsedown in a similar method to how the Jigsaw and Blade classes are now would do it?
Thank you so much for your time, I've been using Jigsaw a lot recently for static sites and it is truly amazing, especially with the recent Mix support :smile: