Automatyczne linki do dokumentacji w Hugo

tech • 1081 słów • 6 minut czytania

W ostatnich moich wpisach o C++ odwoływałem się często do opublikowanego w sieci, w postaci strony HTML, standardu tego języka. Pomyślałem sobie, że zamiast w treści surowego wpisu dodawać bezpośrednie odnośniki do linkowanych fragmentów, dobrym pomysłem będzie używanie identyfikatorów (tych ze standardu) i przerzucenie całej roboty na generator strony. Nie ograniczając się tylko do cpp mógłbym obsłużyć też inne często linkowane dokumentacje.

Taki mechanizm ma też inną zaletę – czystsza treść i prostsza forma wpisu bez linków do dokumentacji, a tym samym bezproblemowa zmiana miejsca docelowego wskazujących odnośników.

Schemat i format odnośników

Na początek trzeba byłoby opracować jakiś “scheme”, własny protokół lub format do zapisu identyfikatorów w treści wpisów służących do automatycznego generowania linków. Ciężko rozszerzyć format Markdowna o nowe elementy w Hugo. Za to można wykorzystać jakieś elementy dla których jest możliwość zdefiniowania własnego Render Hooka, albo dokonywać stosownych poprawek w wygenerowanym HTML-u na etapie “post-produkcji”.

Do tego celu wybrałem styl zapisu odnośników w nawiasach ostrych, w takim formacie:

<doc:[type]:[id]>

gdzie type określa rodzaj dokumentacji, a id specyficzny dla niej identyfikator.

Przykładowo odnośnik do podrozdziału o funkcjach konwersji w dokumentacji standardu C++ może wyglądać tak:

<doc:cpp20:class.conv.fct>

Odnośniki zawarte w nawiasach ostrych są automatycznie zamieniane na linki. Niestety z powodu błędów, obecne implementacje Markdowna w Hugo nie umożliwiają wpływania na taki format automatycznych odnośników za pomocą za pomocą hooka render-link (#6667). Można to jednak poprawić w czasie budowania lub po jakimiś skryptami.

Podobny problem z brakiem obsługi “twardych” linków w render-hookach rozwiązałem zaprzęgając wyrażenia regularne i podmiany stringów w czasie budowania (renderowania) przekształcając wygenerowany kontent takim kodem:

{{- /*
	autolink: <http://link> -> <a href="http://link">link</a>
	https://github.com/gohugoio/hugo/issues/6667
*/ -}}
{{- range (findRE "<(https?://[^>]+)>" .RawContent) }}
	{{- $link := substr . 1 -1 }}
	{{- $old := print ">" $link "</a>" }}
	{{- $link = $link | strings.TrimPrefix "http://" | strings.TrimPrefix "https://" }}
	{{- $new := print ">" $link "</a>" }}
	{{- $content = replace $content $old $new | safeHTML }}
{{- end }}

Moim celem było otrzymanie bardziej czytelnych i lepiej wyglądających odnośników (bez przedrostka protokołu) dla automatycznie generowanych bezpośrednich linków zawartych w plikach z treściami w Markdownie.

Czemu początkowe wyszukiwanie nastepuje po RawContent? Bo chciałem, aby przekształcanie dotyczyło tylko tych jawnie zdefiniowanych przeze mnie linków (<...>), pomijając inne w tym te automatyczne na podstawie treści.

Myślę, że tutaj będę musiał wykorzystać coś podobnego, rozszerzając kod zawarty w pliku render-content.html ;)

Odnośniki do standardu C++

W sieci znajduje się kilka fajnych HTML-owych wersji standardu, automatycznie generowanych z plików źródłowych standardu. Najpopularniejsze to Elisa i Tim Songa, i ten drugi, z uwagi na obsługę w wcześniejszych wersji wykorzystam.

Czasami lepiej będzie się odwoływać do danego punktu/paragrafu, zatem ten wcześniej, przykładowy format doc:link w najbardziej rozbudowanej dla C++ wersji będzie wyglądał jakoś tak:

<doc:cpp20:class.conv.ctor#2>

Taki kod w treści źródłowej powinien przy budowaniu strony zamienić się na taki odnośnik:

<a href="http://timsong-cpp.github.io/cppwp/n4861/class.conv.ctor#2">
	[class.conv.ctor]
</a>

Co łatwo osiągnąć prostym wyrażeniem regularnym i funkcją replaceRE przekształcać wygenerowany output:

{{- /*
	link cpp-std docs from - http://github.com/timsong-cpp/cppwp
	<doc:cpp20:name#id>
*/ -}}
{{- if in .RawContent `<doc:cpp20` }}
	{{- $re := `<a href="[^">]+">doc:cpp20:(([^#>]+)(#[^>])?)</a>` }}
	{{- $str := `<a href="http://timsong-cpp.github.io/cppwp/n4861/$1">[$2]</a>` }}
	{{- $content = replaceRE $re $str $content | safeHTML }}
{{- end }}

Adekwatne fragmenty mógłbym przygotować do pozostałych używanym przeze mnie wersji standardu C++, ale może lepszym rozwiązaniem będzie opracowanie czegoś bardziej generycznego?

Odnośniki do dokumentacji WinAPI

Oprócz głównej idei odnośników do C++, wpadłem na pomysł, że może warto też spróbować zautomatyzować tworzenie odnośników do dokumentacji WinAPI. Biorąc pod uwagę, że czasem, szczególnie w przeszłości, dużo też linkowałem do tej dokumentacji i innych funkcji jakie można obecnie znaleźć na docs.microsoft.com.

Kiedyś, gdy działał jeszcze stary poczciwy MSDN to sprawa mogła być trochę utrudniona, gdyż tam wszelkie adresy w dokumentacji zawierały w sobie identyfikatory niezbyt związane z zawartością. Dla przykładu taka podstrona dokumentująca funkcję CreateWindow miała wtedy taki adres:

http://msdn.microsoft.com/en-us/library/windows/desktop/ms632679.aspx

A obecnie w “nowym” portalu deweloperskim pod adresem:

http://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-createwindowa

Tutaj już widać odzwierciedlenie całej struktury API i bibliotek w dokumentacji i adresie strony.

Na podstawie znajomości ostatniego człona adresu (identyfikatora) można wygenerować docelowy adres do strony, który po szybkim researchu zbudowany jest w takiej postaci (dla API Win32):

http://docs.microsoft.com/en-us/windows/win32/api/[namespace]/[type]-[namespace]-[name]

Pole namespace to rodzaj API lub biblioteki, niekoniecznie fizycznie oddzielona cześć, stąd bardziej odpowiada to przestrzeni nazw. Kolejne ciekawe pole to type określające rodzaj opisywanej części API o podanej nazwie name. Może to być funkcja, callback, struktura, enum, interfejs lub klasa, którym kolejno odpowiadają wartości: nf, nc, ns, ne, nn, nl.

Pomijając dokładne znaczenie i budowę adresu, użycie ostatniego elementu adresu jako identyfikatora w moim schemacie doc:link jest wystarczające. Łatwo go też sparsować reg-exp-em i poprawiać HTML-owy kontent:


{{- /*
	link winapi docs from - http://docs.microsoft.com
	<doc:winapi:id>
*/ -}}

{{- $re := `<a href="[^">]+">doc:winapi:(.*-(.*)-(.*))</a>` }}
{{- $str := `<a href="http://docs.microsoft.com/en-us/windows/win32/api/$2/$1">$3</a>` }}
{{- $content = replaceRE $re $str $content | safeHTML }}

Jedynym mankamentem takiego kodu może być wielkość znaków i liter…

W treściach wpisów musiałbym używać docelowych formatowań w nazwie funkcji (duże/małe litery):

<doc:winapi:nf-processthreadsapi-CreateProcessA>

Żeby w wyniku otrzymać ładny link:

<a href="http://docs.microsoft.../api/processthreadsapi/nf-processthreadsapi-CreateProcessA">
	CreateProcessA
</a>

A to ma efekt uboczny taki, że w adresie też taka forma będzie występować. Na szczęście na router na docs.microsoft.com sobie jakoś radzi z takimi adresami i poprawnie obsłuży takie zapytanie.

Fanie jednak byłoby coś tutaj wymyślić, ale zbytnio nie ma łatwego sposobu na dodatkowe manipulacje.

Generyczne przekształcanie linków

Powielanie wielokrotnie podobnego kodu do obsłużenia różnych typów linków naturalnie wydaje się złym posunięciem. Szczególnie, że prosto można byłoby opracować jakieś generyczne rozwiązanie, bo w końcu zmieniają się tylko jakieś tam parametry – regexpy i sposób przekształceń, a cała reszta - użycie funkcji replaceRE jest zawsze takie same.

Wszystkie parametry i opisy przekształceń zawarłem w prostej liście w formacie YAML:

- re: cpp20:(([^#>]+)(#[^>])?)
  link: http://timsong-cpp.github.io/cppwp/n4861/$1
  name: [$2]

- re: cpp17:(([^#>]+)(#[^>])?)
  link: http://timsong-cpp.github.io/cppwp/n4659/$1
  name: [$2]

...

- re: winapi:(.*-(.*)-(.*))
  link: http://docs.microsoft.com/en-us/windows/win32/api/$2/$1
  name: $3

Może to być osobny plik z danymi (Data Files), co pewnie będzie sensownym rozwiązaniem (jedno parsowanie) lub zawarcie w pliku szablonu i wykorzystanie unmarshal, co też ja uczyniłem w pierwszej i testowej implementacji do testów:

{{
$map := `
  ...
` | transform.Unmarshal
}}

Główny kod przekształcający kontent to iteracja po liście dostępnych formatach doc:link-ów, poskładania z parametrów potrzebnych ciągów i wyrażeń dla funkcji replaceRE i podmiana stringów w treści strony, czyli nic nowego:

{{- range $map }}
	{{- $re := print `<a href="[^">]+">doc:` .re `</a>` }}
	{{- $str := print `<a href="` .link `">` .name `</a>` }}
	{{- $content = replaceRE $re $str $content | safeHTML }}
{{- end }}

Może warto też na początku, przed odpalaniem tego kodu, sprawdzić, czy w treści były jakieś doc:link-i, aby niepotrzebnie nie używać wyrażeń regularnych bo ich obsługa jest jednak trochę kosztowna w Hugo.

Podsumowanie

Automatyczne linkowanie do dokumentacji wydaje mi się fajną opcją, ale przynajmniej na razie zdecydowałem się na nią tylko dla odnośników do standardu C++. Choć nie wykluczone, że kiedyś użyję tego mechanizmu w szerzymy kontekście – WinAPI, a może też i dokumentacja Hugo, do której czasem przy różnych hackach się odwołuję.

Cały proces przekształcania działa w Hugo w czasie generowania strony, co ma tę zaletę, że nie potrzeba żadnych dodatkowych zabiegów przy budowaniu witryny. Minusem są niestety ograniczenia, szczególnie przez możliwości szablonów Go. Wykorzystanie zewnętrznych skryptów w post-produkcji dawałoby znacznie większą kontrole i pełny wpływ na to co i jak robić, ale to znów wymaga zewnętrznych narzędzi. Póki co staram się unikać zewnętrznych skryptów i to co mogę, nawet jakimiś hackami, dokonać w Hugo to z tego korzystam ;)

Komentarze (0)

Dodaj komentarz

/dozwolony markdown/

/nie zostanie opublikowany/