This commit is contained in:
ge 2024-11-03 14:32:04 +03:00
commit 493d23c2f3
17 changed files with 1662 additions and 0 deletions

8
.editorconfig Normal file
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

14
assets/habr-fixer.js Normal file
View 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
View 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

File diff suppressed because one or more lines are too long

89
assets/style.css Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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>

7
v.mod Normal file
View File

@ -0,0 +1,7 @@
Module {
name: 'habraview'
description: 'Habr.com posts viewer'
version: '0.0.1'
license: 'Unlicense'
dependencies: ['whisker']
}