init
This commit is contained in:
		
							
								
								
									
										8
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								.editorconfig
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
[*]
 | 
			
		||||
charset = utf-8
 | 
			
		||||
end_of_line = lf
 | 
			
		||||
insert_final_newline = true
 | 
			
		||||
trim_trailing_whitespace = true
 | 
			
		||||
 | 
			
		||||
[*.v]
 | 
			
		||||
indent_style = tab
 | 
			
		||||
							
								
								
									
										7
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
* text=auto eol=lf
 | 
			
		||||
*.bat eol=crlf
 | 
			
		||||
 | 
			
		||||
**/*.v linguist-language=V
 | 
			
		||||
**/*.vv linguist-language=V
 | 
			
		||||
**/*.vsh linguist-language=V
 | 
			
		||||
**/v.mod linguist-language=V
 | 
			
		||||
							
								
								
									
										20
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
# Binaries for programs and plugins
 | 
			
		||||
habraview
 | 
			
		||||
v
 | 
			
		||||
*.exe
 | 
			
		||||
*.exe~
 | 
			
		||||
*.so
 | 
			
		||||
*.dylib
 | 
			
		||||
*.dll
 | 
			
		||||
 | 
			
		||||
# Ignore binary output folders
 | 
			
		||||
bin/
 | 
			
		||||
 | 
			
		||||
# Ignore common editor/system specific metadata
 | 
			
		||||
.DS_Store
 | 
			
		||||
.idea/
 | 
			
		||||
.vscode/
 | 
			
		||||
*.iml
 | 
			
		||||
 | 
			
		||||
# ENV
 | 
			
		||||
.env
 | 
			
		||||
							
								
								
									
										7
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								Makefile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,7 @@
 | 
			
		||||
all: build
 | 
			
		||||
 | 
			
		||||
build:
 | 
			
		||||
	v -o habraview src
 | 
			
		||||
 | 
			
		||||
prod:
 | 
			
		||||
	v -o habraview -prod -cc clang -compress src
 | 
			
		||||
							
								
								
									
										51
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,51 @@
 | 
			
		||||
# habraview
 | 
			
		||||
 | 
			
		||||
Спартанский микрофронтенд habr.com для возможности архивации страниц
 | 
			
		||||
инструментами вроде ArchiveBox и т.п.
 | 
			
		||||
 | 
			
		||||
Современные фронтенды к Хабру не позволяют сохранить содержимое страницы
 | 
			
		||||
без искажений и в полном объёме. Всё обмазано JavaScript с ленивой загрузкой
 | 
			
		||||
изображений, отчего картинки на сохранённой странице только заблюренные, а
 | 
			
		||||
возможность архивации комментариев полностью отсутствует. Страницы
 | 
			
		||||
альтернативного фронтенда geekr.vercel.app вовсе непригодны для архивации.
 | 
			
		||||
Поэтому появился этот костыль.
 | 
			
		||||
 | 
			
		||||
Фичи:
 | 
			
		||||
 | 
			
		||||
* Умеет отображать страницу в минимальном сносном CSS.
 | 
			
		||||
* Отображает все комментарии (отрисовка дерева не удалась, но и так сойдёт).
 | 
			
		||||
* Решает проблему с заблюренными изображениями.
 | 
			
		||||
 | 
			
		||||
Работает только с `article`, то есть новостные посты и статьи из `sandbox`.
 | 
			
		||||
 | 
			
		||||
# Как пользоваться
 | 
			
		||||
 | 
			
		||||
`habraview` это веб-приложение. Просто запускаем файл:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
./habraview
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Приложение будет по умолчанию будет слушать на 8888 порту. Чтобы получить
 | 
			
		||||
страницу, открываем в брайзере:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
http://localhost:8888?url=https://habr.com/ru/articles/853062/
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Адрес статьи на Хабре можно передать целиком как значение quey-параметра `url`
 | 
			
		||||
или как `id`:
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
http://localhost:8888?id=853062
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
Теперь на эту страницу можно натравить архиватор веб-страниц.
 | 
			
		||||
 | 
			
		||||
# Компиляция
 | 
			
		||||
 | 
			
		||||
Нужны компиляторы `gcc` и [v](https://vlang.io):
 | 
			
		||||
 | 
			
		||||
```
 | 
			
		||||
v -prod -cflags -static -cflags -s .
 | 
			
		||||
```
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								assets/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 7.2 KiB  | 
							
								
								
									
										14
									
								
								assets/habr-fixer.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								assets/habr-fixer.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,14 @@
 | 
			
		||||
(function(document)
 | 
			
		||||
{
 | 
			
		||||
    const onDOMBuild = () =>
 | 
			
		||||
    {
 | 
			
		||||
        // Fix blurred images
 | 
			
		||||
        const images = document.querySelectorAll('img[data-blurred="true"]');
 | 
			
		||||
        for (let image of images)
 | 
			
		||||
        {
 | 
			
		||||
            image.src = image.dataset.src;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    document.addEventListener('DOMContentLoaded', onDOMBuild);
 | 
			
		||||
})(document);
 | 
			
		||||
							
								
								
									
										9
									
								
								assets/highlight.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								assets/highlight.min.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
/*!
 | 
			
		||||
  Theme: Default
 | 
			
		||||
  Description: Original highlight.js style
 | 
			
		||||
  Author: (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
 | 
			
		||||
  Maintainer: @highlightjs/core-team
 | 
			
		||||
  Website: https://highlightjs.org/
 | 
			
		||||
  License: see project LICENSE
 | 
			
		||||
  Touched: 2021
 | 
			
		||||
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#f3f3f3;color:#444}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
 | 
			
		||||
							
								
								
									
										1213
									
								
								assets/highlight.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1213
									
								
								assets/highlight.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										89
									
								
								assets/style.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								assets/style.css
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,89 @@
 | 
			
		||||
body {
 | 
			
		||||
  background-color: #F1F5F9;
 | 
			
		||||
  font-family: sans-serif;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
header, main, footer {
 | 
			
		||||
  margin: 0 auto;
 | 
			
		||||
  max-width: 1080px;
 | 
			
		||||
}
 | 
			
		||||
article {
 | 
			
		||||
  background-color: white;
 | 
			
		||||
  padding: 0.5rem 1.5rem;
 | 
			
		||||
  border: 1px solid #E6E7E9;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
}
 | 
			
		||||
pre code.hljs {
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
}
 | 
			
		||||
article img {
 | 
			
		||||
  height: 100% !important;
 | 
			
		||||
  width: 100% !important;
 | 
			
		||||
  object-fit: scale-down;
 | 
			
		||||
}
 | 
			
		||||
blockquote {
 | 
			
		||||
  border-left: 4px solid #bbbbbb;
 | 
			
		||||
  padding-left: 8px;
 | 
			
		||||
  margin-left: 4px;
 | 
			
		||||
}
 | 
			
		||||
figure {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
}
 | 
			
		||||
figcaption {
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  opacity: 60%;
 | 
			
		||||
}
 | 
			
		||||
#date-published {
 | 
			
		||||
  opacity: 60%;
 | 
			
		||||
}
 | 
			
		||||
#article-title {
 | 
			
		||||
  font-size: 28px;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
}
 | 
			
		||||
#tags {
 | 
			
		||||
  margin: 1rem 0;
 | 
			
		||||
}
 | 
			
		||||
#hubs {
 | 
			
		||||
  margin: 1rem 0;
 | 
			
		||||
}
 | 
			
		||||
span.comma:not(:empty) ~ span.comma:not(:empty):before {
 | 
			
		||||
  content: ", ";
 | 
			
		||||
}
 | 
			
		||||
#comments {
 | 
			
		||||
  margin-top: 1rem;
 | 
			
		||||
  background-color: white;
 | 
			
		||||
  padding: 0.5rem 1.5rem;
 | 
			
		||||
  border: 1px solid #E6E7E9;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
}
 | 
			
		||||
.comment {
 | 
			
		||||
  padding: 0.5rem 0;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
}
 | 
			
		||||
.comment-header {
 | 
			
		||||
  background-color: #f5f5f5;
 | 
			
		||||
}
 | 
			
		||||
.replies-list {
 | 
			
		||||
  margin: 1rem 0;
 | 
			
		||||
}
 | 
			
		||||
ul.inline-list {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
}
 | 
			
		||||
ul.inline-list li {
 | 
			
		||||
  display: inline;
 | 
			
		||||
  list-style: none;
 | 
			
		||||
}
 | 
			
		||||
table, th, td {
 | 
			
		||||
  border: 1px solid #E6E7E9;
 | 
			
		||||
  border-collapse: collapse;
 | 
			
		||||
  padding: 2px 16px;
 | 
			
		||||
}
 | 
			
		||||
:target {
 | 
			
		||||
  border-left: 4px solid black;
 | 
			
		||||
  padding: 0 1rem !important;
 | 
			
		||||
  background-color: #f8f8f8;
 | 
			
		||||
  .comment-header {
 | 
			
		||||
    background-color: #cccccc;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								habr/api.v
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								habr/api.v
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
module habr
 | 
			
		||||
 | 
			
		||||
import net.http
 | 
			
		||||
 | 
			
		||||
pub struct Habr {
 | 
			
		||||
	baseurl string = $d('habr_baseurl', 'https://habr.com')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn Habr.new() Habr {
 | 
			
		||||
	return Habr{}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn (h Habr) get_article(id int) !string {
 | 
			
		||||
	response := http.get('${h.baseurl}/kek/v2/articles/${id}/') or { return err }
 | 
			
		||||
	return response.body
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn (h Habr) get_article_comments(id int) !string {
 | 
			
		||||
	response := http.get('${h.baseurl}/kek/v2/articles/${id}/comments/') or { return err }
 | 
			
		||||
	return response.body
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								habr/article.v
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								habr/article.v
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
module habr
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
import veb { RawHtml }
 | 
			
		||||
 | 
			
		||||
pub struct Article {
 | 
			
		||||
pub:
 | 
			
		||||
	published_at string  @[json: timePublished]
 | 
			
		||||
	title        string  @[json: titleHtml]
 | 
			
		||||
	text         RawHtml @[json: textHtml]
 | 
			
		||||
	hubs         []Hub
 | 
			
		||||
	tags         []Tag
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn Article.parse(input string) Article {
 | 
			
		||||
	return json.decode(Article, input) or { Article{} }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct Hub {
 | 
			
		||||
pub:
 | 
			
		||||
	id    string
 | 
			
		||||
	alias string
 | 
			
		||||
	title string @[json: titleHtml]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct Tag {
 | 
			
		||||
pub:
 | 
			
		||||
	title string @[json: titleHtml]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct Comment {
 | 
			
		||||
pub:
 | 
			
		||||
	id          string
 | 
			
		||||
	parent_id   string   @[json: parentId]
 | 
			
		||||
	replies_ids []string @[json: children]
 | 
			
		||||
	level       int
 | 
			
		||||
	author      CommentAuthor
 | 
			
		||||
	message     RawHtml
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct CommentAuthor {
 | 
			
		||||
pub:
 | 
			
		||||
	alias string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct CommentsMapped {
 | 
			
		||||
mut:
 | 
			
		||||
	items map[string]Comment @[json: comments]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct Comments {
 | 
			
		||||
pub mut:
 | 
			
		||||
	items []Comment
 | 
			
		||||
mut:
 | 
			
		||||
	idx int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn Comments.parse(input string) Comments {
 | 
			
		||||
	mut comments := Comments{}
 | 
			
		||||
	mapped := json.decode(CommentsMapped, input) or { CommentsMapped{} }
 | 
			
		||||
	for _, v in mapped.items {
 | 
			
		||||
		comments.items << v
 | 
			
		||||
	}
 | 
			
		||||
	return comments
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								habr/util.v
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								habr/util.v
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,12 @@
 | 
			
		||||
module habr
 | 
			
		||||
 | 
			
		||||
import regex
 | 
			
		||||
 | 
			
		||||
pub fn get_id_from_url(url string) ?string {
 | 
			
		||||
	mut re := regex.regex_opt(r'/\d+/?$') or { return none }
 | 
			
		||||
	begin, end := re.find(url)
 | 
			
		||||
	if begin > 0 && end > 0 {
 | 
			
		||||
		return url[begin..end].trim('/')
 | 
			
		||||
	}
 | 
			
		||||
	return none
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										67
									
								
								habraview.v
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								habraview.v
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
			
		||||
module main
 | 
			
		||||
 | 
			
		||||
import cli
 | 
			
		||||
import habr
 | 
			
		||||
import os
 | 
			
		||||
import veb
 | 
			
		||||
 | 
			
		||||
pub struct Context {
 | 
			
		||||
	veb.Context
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct App {
 | 
			
		||||
	veb.StaticHandler
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
struct Response {
 | 
			
		||||
	msg string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@[get]
 | 
			
		||||
fn (a &App) index(mut ctx Context) veb.Result {
 | 
			
		||||
	article_id := ctx.query['id'] or { habr.get_id_from_url(ctx.query['url']) or { '' } }
 | 
			
		||||
	client := habr.Habr.new()
 | 
			
		||||
	raw_article := client.get_article(article_id.int()) or {
 | 
			
		||||
		return ctx.json(Response{ msg: err.str() })
 | 
			
		||||
	}
 | 
			
		||||
	raw_comments := client.get_article_comments(article_id.int()) or {
 | 
			
		||||
		return ctx.json(Response{ msg: err.str() })
 | 
			
		||||
	}
 | 
			
		||||
	article := habr.Article.parse(raw_article)
 | 
			
		||||
	comments := habr.Comments.parse(raw_comments)
 | 
			
		||||
	return $veb.html()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn runserver(port int) ! {
 | 
			
		||||
	os.chdir(os.dir(@FILE))!
 | 
			
		||||
	mut app := &App{}
 | 
			
		||||
	app.handle_static('assets', false)!
 | 
			
		||||
	app.serve_static('/favicon.ico', 'assets/favicon.ico')!
 | 
			
		||||
	veb.run[App, Context](mut app, port)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn main() {
 | 
			
		||||
	mut app := cli.Command{
 | 
			
		||||
		name:        'habraview'
 | 
			
		||||
		description: 'Habr.com posts viewer.'
 | 
			
		||||
		version:     $d('habraview_version', '0.0.0')
 | 
			
		||||
		defaults:    struct {
 | 
			
		||||
			man: false
 | 
			
		||||
		}
 | 
			
		||||
		execute:     fn (cmd cli.Command) ! {
 | 
			
		||||
			port := cmd.flags.get_int('port') or { 8080 }
 | 
			
		||||
			runserver(port)!
 | 
			
		||||
		}
 | 
			
		||||
		flags:       [
 | 
			
		||||
			cli.Flag{
 | 
			
		||||
				flag:          .int
 | 
			
		||||
				name:          'port'
 | 
			
		||||
				abbrev:        'p'
 | 
			
		||||
				description:   'Listen port [default: 8888].'
 | 
			
		||||
				default_value: ['8888']
 | 
			
		||||
			},
 | 
			
		||||
		]
 | 
			
		||||
	}
 | 
			
		||||
	app.setup()
 | 
			
		||||
	app.parse(os.args)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								templates/comment.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								templates/comment.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
<div class="comment" id="@{comment.id}">
 | 
			
		||||
  <p class="comment-header">[<a href="#@{comment.id}">@{comment.id}</a>] <strong>@{comment.author.alias}</strong>
 | 
			
		||||
    @if comment.parent_id != ''
 | 
			
		||||
    <span> in reply to <a href="#@{comment.parent_id}">#@{comment.parent_id}</a></span>
 | 
			
		||||
    @end
 | 
			
		||||
  </p>
 | 
			
		||||
  <p>@{comment.message}</p>
 | 
			
		||||
  @if comment.replies_ids.len > 0  
 | 
			
		||||
  <div class="replies-list">
 | 
			
		||||
    <span>Replies: </span>
 | 
			
		||||
    @for reply_id in comment.replies_ids
 | 
			
		||||
    <span><a href="#@{reply_id}">#@{reply_id}</a></span>
 | 
			
		||||
    @end
 | 
			
		||||
  </div>
 | 
			
		||||
  @end
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										56
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								templates/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="UTF-8">
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
  <title>@{article.title}</title>
 | 
			
		||||
  @css '/assets/style.css'
 | 
			
		||||
  @css '/assets/highlight.min.css'
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <nav></nav>
 | 
			
		||||
  <header>
 | 
			
		||||
    <h1 id="article-title">@{article.title}</h1>
 | 
			
		||||
    <p id="date-published">@{article.published_at}</p>
 | 
			
		||||
  </header>
 | 
			
		||||
  <main>
 | 
			
		||||
    <article>
 | 
			
		||||
      @{article.text}
 | 
			
		||||
    </article>
 | 
			
		||||
    <div id="tags">
 | 
			
		||||
    @if article.tags.len > 0
 | 
			
		||||
      <span><strong>Тэги: </strong></span>
 | 
			
		||||
      @for tag in article.tags
 | 
			
		||||
      <span class="comma">
 | 
			
		||||
        <a href="https://habr.com/ru/search/?target_type=posts&order=relevance&q=%5B@{tag.title}%5D">
 | 
			
		||||
          @{tag.title}
 | 
			
		||||
        </a>
 | 
			
		||||
      </span>
 | 
			
		||||
      @end
 | 
			
		||||
    @end
 | 
			
		||||
    </div>
 | 
			
		||||
    <div id="hubs">
 | 
			
		||||
    @if article.hubs.len > 0
 | 
			
		||||
      <span><strong>Хабы: </strong></span>
 | 
			
		||||
      @for hub in article.hubs
 | 
			
		||||
      <span class="comma">
 | 
			
		||||
        <a href="https://habr.com/ru/hubs/@{hub.alias}/articles/">
 | 
			
		||||
          @{hub.title}
 | 
			
		||||
        </a>
 | 
			
		||||
      </span>
 | 
			
		||||
      @end
 | 
			
		||||
    @end
 | 
			
		||||
    </div>
 | 
			
		||||
    <div id="comments">
 | 
			
		||||
      <p><strong>Комментарии</strong> (@{comments.items.len})</p>
 | 
			
		||||
      @for comment in comments.items
 | 
			
		||||
        @include 'comment.html'
 | 
			
		||||
      @end
 | 
			
		||||
    <div>
 | 
			
		||||
  </main>
 | 
			
		||||
  <footer>
 | 
			
		||||
  </footer>
 | 
			
		||||
  @js '/assets/habr-fixer.js'
 | 
			
		||||
  @js '/assets/highlight.min.js'
 | 
			
		||||
  <script>hljs.highlightAll();</script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
		Reference in New Issue
	
	Block a user