forked from uhub/awesome-lua
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathinit.lua
498 lines (408 loc) · 15.6 KB
/
init.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
-- local line = ""
-- local repo_url, description = line:match("%* %[.-%]%((https://github%.com/.-)%) %- (.+)$")
-- print(repo_url)
-- if 1 then return end
local function file_write(path, content)
local file, err = io.open(path, "w")
if err and err:find("No such file or directory") then
local dir = path:match("(.+)/[^/]+$")
os.execute("mkdir -p " .. dir)
file, err = io.open(path, "w")
end
assert(file, err)
file:write(content)
file:close()
end
local function file_read(path)
local file = io.open(path, "r")
if file then
local content = file:read("*a")
file:close()
return content
end
end
--- @class ParsedHeaderItem
--- @field repo_url string
--- @field description string
--- @field owner string
--- @field repo string
--- @field info ExtendedItemInfo
--- @class ParsedHeader
--- @field header_level number
--- @field header_name string
--- @field items ParsedHeaderItem[]
--- @alias ParsedHeaders ParsedHeader[]
--- @return ParsedHeaders
local function parseReadmeFile(filepath)
local file = assert( io.open(filepath, "r") )
local headers = {}
local currentHeader = nil
for line in file:lines() do
local level, name = line:match("^(#+)%s+(.+)$")
if level and name then
currentHeader = {header_level = #level, header_name = name, items = {}}
table.insert(headers, currentHeader)
elseif currentHeader then
local repo_url, description = line:match("%* %[.-%]%((https://github%.com/.-)%) %- (.+)$")
if repo_url and description then
local owner, repo = repo_url:match("https://github%.com/(.-)/(.+)")
assert(repo_url == "https://github.com/" .. owner .. "/" .. repo)
table.insert(currentHeader.items, {
repo_url = repo_url,
description = description,
owner = owner,
repo = repo
})
-- else
-- print("🆘 " .. line) -- TOC, empty lines, comments
end
end
end
file:close()
return headers
end
local GITHUB_TOKEN = assert(os.getenv("GITHUB_TOKEN"), "GITHUB_TOKEN env is not set")
local DEV = os.getenv("LUA_ENV") == "development"
local parsed_headers = parseReadmeFile(arg[1] or "./README.md")
-- local print = require("tlib").PRINT
local copas = require("copas")
local request = require("http_v2").copas_request
local json = require("cjson")
-- local base64 = require("base64")
local function github_request(token, endpoint)
if DEV then
local file_path = "./apicache/github_api_responses" .. endpoint .. ".json"
local content = file_read(file_path)
if content then
return json.decode(content)
end
end
-- local url = "https://api.github.com" .. endpoint
local url = "https://awesome-lua.amd.workers.dev" .. endpoint
local body, code, headers = request("GET", url, nil, {
["User-Agent"] = "Mozilla/5.0 (compatible; Lua; Windows NT)",
["Accept"] = "application/vnd.github.v3+json",
["Authorization"] = "Bearer " .. token,
})
assert(body and code == 200, "Request failed: " .. url .. "\n" .. body .. "\n" .. code)
if DEV then
local file_path = "./apicache/github_api_responses" .. endpoint .. ".json"
file_write(file_path, body)
end
local response = json.decode(body)
return response, headers and headers["x-ratelimit-limit"] and { -- not exists for cached responses
rate_limit_limit = tonumber(headers["x-ratelimit-limit"]),
rate_limit_reset = tonumber(headers["x-ratelimit-reset"]),
rate_limit_used = tonumber(headers["x-ratelimit-used"]),
}
end
local function get_repo_info(owner, repo)
return github_request(GITHUB_TOKEN, "/repos/" .. owner .. "/" .. repo)
end
-- local function get_readme(owner, repo)
-- local response, limits = github_request(GITHUB_TOKEN, "/repos/" .. owner .. "/" .. repo .. "/readme")
-- assert(response.encoding == "base64", "Unknown encoding: " .. response.encoding)
-- response.content = base64.decode(response.content)
-- return response, limits
-- end
-- copas.loop(function()
-- print( get_repo_info("TRIGONIM", "ggram") )
-- end)
--- @param item ParsedHeaderItem
local function calc_item_score(item)
local score = 0
score = score + math.sqrt(item.info.stars) -- sqrt 1 = 1, 10 = 3.16, 100 = 10, 1000 = 31.6, 10000 = 100
-- score = score + item.info.subscribers
-- newbies boost
do
local age_month = (os.time() - item.info.created_at) / 86400 / 30
local delta = math.min(age_month, 12) / 12 -- 0..1 higher is better
score = score + delta * 10
end
-- grow speed impact
do
local age_month = (os.time() - item.info.created_at) / 86400 / 30
local stars_per_month = item.info.stars / age_month
local delta = math.min(stars_per_month, 20) / 20 -- 0..1 higher is better
score = score + delta * 10
end
-- last activity
do
-- local delta = 1 - math.min(item.info.last_commit_days_ago, 90) / 90 -- 0..1 higher is better
local delta = 1 - math.min(item.info.last_commit_days_ago, 180) / 90 -- -1..1 higher is better
score = score + math.max(delta * 30, -5) -- -5..30
end
if item.info.archived then
score = score - 20
end
return score
end
-- print(calc_item_score({
-- info = {
-- stars = 30,
-- -- subscribers = 100,
-- created_at = os.time() - 86400 * 30 * 3, -- N months ago
-- last_commit_days_ago = 10,
-- },
-- })) if 1 then return end
local function sort_items_by_score(items)
table.sort(items, function(a, b) return calc_item_score(a) > calc_item_score(b) end)
end
--- @param all_headers ParsedHeaders
--- @return number total_items
local function count_items(all_headers)
local total_items = 0
for _, header in ipairs( all_headers ) do total_items = total_items + #header.items end
return total_items
end
local function escape_description(str)
return ( str:gsub("<", "\\<") ) -- "REST <-> gRPC gateway" ==> "\<-> gRPC"
end
local function string_interpolate(str, values)
-- for k, v in pairs(values) do str = str:gsub("{" .. k .. "}", v) end
-- return str
return (str:gsub("{([^}]+)}", values))
end
-- print( string_interpolate(spoiler_content_pattern, {subscribers = math.floor(123.0)}) )
-- if 1 then return end
local file_struct_template = [[
---
{front_matter}
---
{main_header}
{meta}
:::tip[**Welcome to the collection of Lua repositories!** 👋]
All repositories are **automatically** sorted by a specific `Score`, which takes into account the date of the last commit, the number of stars, and also gives a slight advantage to repositories that have been recently created.
The meta-information about repositories is **automatically updated** regularly. \
The generator for these pages is also written in **Lua** 🌑
You can add your own or someone else's repository to the list by clicking the green button at the top or by creating an Issue. For every repository you add, you get 9000 love from me ❤️. It's easy!
I would also welcome your suggestions on how to improve the structuring of repositories.
:::
:::note[Circles Legend]
```
Last commit..
⚪ 0-7 days ago 🟢 8-30 days ago
🟡 31-60 days ago 🟠 61-90 days ago
🟤 91-180 days ago 🔴 181-365 days ago
⚫ 366+ days ago
```
:::
{main_body}]]
local actuality_circles = {"⚪", "🟢", "🟡", "🟠", "🟤", "🔴", "⚫"} -- "🟣", "🔵",
local get_circle = function(days_ago)
local circle_i = 0 -- ugly but works
if days_ago < 8 then circle_i = 1
elseif days_ago < 31 then circle_i = 2
elseif days_ago < 61 then circle_i = 3
elseif days_ago < 91 then circle_i = 4
elseif days_ago < 181 then circle_i = 5
elseif days_ago < 366 then circle_i = 6
else circle_i = 7
end
return actuality_circles[circle_i]
end
local generate_spoiled_item do
local spoiler_pat = [[
<details>
<summary>%s [%s](https://github.com/%s) – %s</summary>
%s
</details>
]]
local spoiler_content_pattern = [[
**Topics**: {_topics_str_md} \
**Watchers**: {subscribers} **Forks**: {forks} **Stars**: {stars} **Issues**: {issues} \
**Last commit**: {_last_commit_str} ({last_commit_days_ago} days ago) \
**Created at**: {_created_at_str} \
**License**: {_license_name}]]
--- @param item ParsedHeaderItem
local function generate_item_spoiler_content(item)
local ex = item.info --- @type table
ex._last_commit_str = os.date("%Y-%m-%d", ex.last_commit)
ex._created_at_str = os.date("%Y-%m-%d", ex.created_at)
ex._topics_str_md = next(ex.topics) and ("`" .. table.concat(ex.topics, "`, `") .. "`") or "none"
ex._license_name = ex.license and ex.license.name or "none"
-- ex._archived_str = ex.archived and "yes" or "no"
return string_interpolate(spoiler_content_pattern, ex)
end
local function item_spoiler(emoji, owner_repo, description, content)
return string.format(spoiler_pat, emoji, owner_repo, owner_repo, description, content)
end
generate_spoiled_item = function(item)
local actuality_emoji = get_circle(item.info.last_commit_days_ago)
local score_str = DEV and (" " .. math.floor(calc_item_score(item))) or ""
return item_spoiler(
actuality_emoji .. score_str,
item.owner .. "/" .. item.repo,
escape_description(item.info.description),
generate_item_spoiler_content(item)
)
end
end
local function generate_list_item(item)
local owner_repo = item.owner .. "/" .. item.repo
-- local last_commit_shield_url = "https://img.shields.io/github/last-commit/" .. owner_repo
-- local last_commit_shield_md = string.format("![%s](%s)", owner_repo, last_commit_shield_url)
local repo_url_md = string.format("[%s](%s)", owner_repo, item.repo_url)
local actuality_emoji = get_circle(item.info.last_commit_days_ago)
return
"- " .. actuality_emoji .. " " .. repo_url_md .. "\n" ..
" " .. escape_description(item.info.description)
end
local get_meta do
-- local meta_pat = [[
-- **Total items**: {total_items} \
-- **Last update**: {last_update} \
-- **Add repo**: [click](https://github.com/AMD-NICK/awesome-lua/edit/master/README.md)]]
local meta_pat = [[
[![add repo](https://img.shields.io/badge/Add_Your_Repo-green?style=for-the-badge&logo=github)](https://github.com/AMD-NICK/awesome-lua/edit/master/README.md)
![last update](https://img.shields.io/badge/Last_Update-{last_update}-blue?style=for-the-badge)
![total items](https://img.shields.io/badge/Total_Items-{total_items}-blue?style=for-the-badge&logo=awesomelists)
]]
get_meta = function(page_struct)
return string_interpolate(meta_pat, {
total_items = count_items(page_struct),
last_update = os.date("%Y/%m/%d"),
})
end
end
local function create_list_of_formatted_items(items, formatter)
local body = ""
for _, item in ipairs(items) do
body = body .. formatter(item) .. "\n"
end
return body
end
local function create_body(category_tree, item_formatter)
local body = ""
for _, category in ipairs(category_tree) do
sort_items_by_score(category.items)
local data = ""
if category.header_level > 1 then
data = string.rep("#", category.header_level) .. " " .. category.header_name .. "\n\n"
end
if #category.items > 0 then
data = data .. create_list_of_formatted_items(category.items, item_formatter) .. "\n"
end
body = body .. data
end
return body
end
local function generate_slug(str)
return str
:gsub("%s", "-"):gsub("[^%w%-_]", ""):lower()
:gsub("^%-+", ""):gsub("%-+$", "") -- trailing "-"
:gsub("%-+", "-") -- multiple "--" to "-"
end
local sidebar_position = 0
--- @param category_tree ParsedHeaders
local function create_category_file(category_tree)
sidebar_position = sidebar_position + 1
local content = string_interpolate(file_struct_template, {
front_matter = "sidebar_position: " .. sidebar_position,
main_header = "# " .. category_tree[1].header_name,
meta = get_meta(category_tree),
main_body = create_body(category_tree, generate_spoiled_item),
})
local slug = generate_slug(category_tree[1].header_name)
file_write((arg[2] or "site/docs"):gsub("/$", "") .. "/" .. slug .. ".md", content)
end
--- @param category_tree ParsedHeaders
local function create_index_file(category_tree)
sidebar_position = sidebar_position + 1 -- always 1 here
local content = string_interpolate(file_struct_template, {
front_matter = "title: All In One Simple List\nslug: /\nsidebar_position: " .. sidebar_position,
main_header = "# " .. category_tree[1].header_name,
meta = get_meta(category_tree),
main_body = create_body(category_tree, generate_list_item),
})
file_write((arg[2] or "site/docs"):gsub("/$", "") .. "/index.md", content)
end
local function parse_date(str) -- 2023-11-30T12:02:18Z to timestamp
local year, month, day, hour, min, sec = str:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)Z")
return os.time({year = year, month = month, day = day, hour = hour, min = min, sec = sec})
end
local function pick_extended_info(full_info)
-- print({full_info = full_info})
local last_commit = parse_date(full_info.pushed_at)
local extended_item_info = { --- @class ExtendedItemInfo
description = full_info.description, --- @type string repo description (not readme)
stars = math.floor(full_info.stargazers_count),
forks = math.floor(full_info.forks_count),
subscribers = math.floor(full_info.subscribers_count), -- watchers
issues = math.floor(full_info.open_issues_count), -- open
last_commit = last_commit,
last_commit_days_ago = math.floor((os.time() - last_commit) / 86400),
topics = full_info.topics, --- @type string[] tags (topics)
created_at = parse_date(full_info.created_at),
archived = full_info.archived, --- @type boolean
license = type(full_info.license) == "table" and full_info.license or nil, --- @type table | nil userdata (null). cjson
}
return extended_item_info
end
--- @param item ParsedHeaderItem
local function get_extended_item_info(item)
local info, rate_limits = get_repo_info(item.owner, item.repo)
-- print("owner_repo", item.owner .. "/" .. item.repo)
local extended_info = pick_extended_info(info)
-- item.info = filtered_info
return extended_info, rate_limits
end
--- @param all_headers ParsedHeaders
local function extend_all_items_async(all_headers, callback)
local total_items = count_items(all_headers)
local done = 0
for _, header in ipairs(all_headers) do
for _, item in ipairs(header.items) do
copas.pause(0) -- prevents copas.lua:1510: bad argument #2 to 'select' (descriptor too large for set size)
copas.addthread(function()
local inf, limits = get_extended_item_info(item)
local limits_str = limits and string.format("Rate limits: %d/%d, reset in %d seconds",
limits.rate_limit_used, limits.rate_limit_limit, limits.rate_limit_reset - os.time()) or "Rate limits: unknown" -- unknown in case of DEV
item.info = inf
done = done + 1
print("[" .. done .. "/" .. total_items .. "] " .. item.owner .. "/" .. item.repo .. "\n\t" .. limits_str)
if done == total_items then
callback()
end
end)
end
end
end
copas.addthread(function()
extend_all_items_async(parsed_headers, function()
print("extend_all_items_async Done ✅")
-- local flatten = {{
-- header_level = 1,
-- header_name = "Uncategorized",
-- items = {}
-- }}
-- for _, header in ipairs(parsed_headers) do
-- for _, item in ipairs(header.items) do
-- table.insert(flatten[1].items, item)
-- end
-- end
-- create_index_file(flatten)
create_index_file(parsed_headers)
local deep = 0
local page_headers = {}
for i, category in ipairs(parsed_headers) do
local categ_minus = setmetatable({
header_level = category.header_level - 1,
}, {__index = category})
if category.header_level == 1 then -- main header
-- do nothing. continue. #todo
elseif category.header_level >= deep then -- include to current page
table.insert(page_headers, categ_minus)
else
create_category_file(page_headers)
page_headers = { categ_minus } -- new page
if i == #parsed_headers then -- create last page (the unsorted page)
create_category_file(page_headers)
end
end
deep = category.header_level
end
end)
end)
-- print(parsed_headers)
copas.loop()