Init
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
downloads/
|
||||||
|
export/
|
||||||
|
html/
|
||||||
|
html_data/
|
||||||
|
html_data2/
|
||||||
|
repo
|
||||||
|
rockyman/
|
||||||
|
tmp/
|
||||||
39
README.md
Normal file
39
README.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
To create a persistent pod
|
||||||
|
|
||||||
|
```
|
||||||
|
podman create -it --name rocky-9-man -v $(pwd):/data/ rockylinux:9 /bin/bash
|
||||||
|
podman exec -it rocky-9-man /bin/bash
|
||||||
|
```
|
||||||
|
|
||||||
|
To create a temp pod
|
||||||
|
|
||||||
|
```
|
||||||
|
podman run --rm -it -v $(pwd):/data/ rockylinux:9 /bin/bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Then `cd /data`
|
||||||
|
|
||||||
|
Install Dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
dnf install -y epel-release
|
||||||
|
dnf install -y python3 python3-dnf python3-rpm python3-requests python3-pip python3-jinja2 python3-aiohttp python3-zstandard mandoc
|
||||||
|
```
|
||||||
|
|
||||||
|
Set alternative python if you need to
|
||||||
|
|
||||||
|
```
|
||||||
|
alternatives --set python $(which python3)
|
||||||
|
```
|
||||||
|
|
||||||
|
And run
|
||||||
|
```
|
||||||
|
python3 rocky_man.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This will download all appstream and baseos for 9.5 and 8.10 into ./tmp and the finished html will be saved to ./html.
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
- Add async
|
||||||
|
- Investigate "Error downloading package: 'utf-8' codec can't decode byte 0xe2 in position 220: invalid continuation byte"
|
||||||
|
- Delete files after they have been processed or at the end
|
||||||
134
old_scripts/apply_template.py
Normal file
134
old_scripts/apply_template.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
# Simplified CSS with meaningful class names
|
||||||
|
FILTERED_CSS = """
|
||||||
|
/* General Styles */
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #0D0A09;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Styles */
|
||||||
|
.header {
|
||||||
|
background-color: #0FB981;
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Styles */
|
||||||
|
.main-content {
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #282828;
|
||||||
|
color: white;
|
||||||
|
max-width: 800px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content a {
|
||||||
|
color: #0FB981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head-vol {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Adjustments */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.main-content {
|
||||||
|
margin: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define the HTML template with placeholders for title, nav, left pane, content, and right pane
|
||||||
|
HTML_TEMPLATE = """<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{file_name} - {rpm_name} - Rocky Man Page</title>
|
||||||
|
<style>
|
||||||
|
{css}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<h1>{file_name}</h1>
|
||||||
|
</header>
|
||||||
|
<main class="main-content">
|
||||||
|
{content}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"""
|
||||||
|
|
||||||
|
def clean_html(html_content):
|
||||||
|
"""
|
||||||
|
Removes existing <html>, <head>, and <body> tags from the HTML content.
|
||||||
|
"""
|
||||||
|
html_content = re.sub(r'</?html[^>]*>', '', html_content, flags=re.IGNORECASE)
|
||||||
|
html_content = re.sub(r'</?head[^>]*>', '', html_content, flags=re.IGNORECASE)
|
||||||
|
html_content = re.sub(r'</?body[^>]*>', '', html_content, flags=re.IGNORECASE)
|
||||||
|
return html_content.strip()
|
||||||
|
|
||||||
|
def add_see_also_links(html_content):
|
||||||
|
"""
|
||||||
|
Adds hyperlinks to existing See Also sections in the HTML content.
|
||||||
|
"""
|
||||||
|
soup = BeautifulSoup(html_content, 'html.parser')
|
||||||
|
|
||||||
|
# Locate the section
|
||||||
|
sections = soup.find_all('section', class_='Sh')
|
||||||
|
|
||||||
|
# Loop through sections to find the one with "SEE ALSO"
|
||||||
|
for section in sections:
|
||||||
|
heading = section.find('h1', id="SEE_ALSO") # Look for the specific "SEE ALSO" heading
|
||||||
|
if heading: # If the heading exists in this section
|
||||||
|
extracted_content = []
|
||||||
|
for b_tag in section.find_all('b'):
|
||||||
|
text_with_parentheses = b_tag.get_text() + b_tag.next_sibling.strip() # Combine <b> text and next sibling
|
||||||
|
extracted_content.append(text_with_parentheses)
|
||||||
|
print(extracted_content)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="Wrap HTML content with a consistent theme including nav, left pane, and right pane.")
|
||||||
|
parser.add_argument('--rpm_name', type=str, help="RPM Name")
|
||||||
|
parser.add_argument('--file_name', type=str, help="File Name")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# Read HTML content from stdin
|
||||||
|
input_html = sys.stdin.read()
|
||||||
|
|
||||||
|
# Extract or set the title
|
||||||
|
rpm_name = args.rpm_name
|
||||||
|
file_name = args.file_name
|
||||||
|
|
||||||
|
# Clean the HTML content
|
||||||
|
cleaned_content = clean_html(input_html)
|
||||||
|
|
||||||
|
# Add See Also links
|
||||||
|
content_with_links = add_see_also_links(cleaned_content)
|
||||||
|
|
||||||
|
# Fill the HTML template
|
||||||
|
themed_html = HTML_TEMPLATE.format(
|
||||||
|
rpm_name=rpm_name,
|
||||||
|
css=FILTERED_CSS,
|
||||||
|
file_name=file_name,
|
||||||
|
content=content_with_links
|
||||||
|
)
|
||||||
|
|
||||||
|
# Output the themed HTML to stdout
|
||||||
|
print(themed_html)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
48
old_scripts/convert_man.py
Normal file
48
old_scripts/convert_man.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROCKY_VERSION = "8.10"
|
||||||
|
MAN_PATH = f"./export/{ROCKY_VERSION}/"
|
||||||
|
HTML_BASE_PATH = f"./html_data2/{ROCKY_VERSION}/"
|
||||||
|
|
||||||
|
def process_file(file):
|
||||||
|
rpm_name = file.parts[3]
|
||||||
|
man_context = file.parts[7]
|
||||||
|
man_filename = file.name.replace('.gz', '').rsplit('.', 1)[0]
|
||||||
|
|
||||||
|
output_folder = Path(HTML_BASE_PATH) / rpm_name / man_context
|
||||||
|
output_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
print(man_filename)
|
||||||
|
|
||||||
|
try:
|
||||||
|
html_content = subprocess.check_output(
|
||||||
|
f'zcat "{file}" | mandoc -T html -O fragment 2>/tmp/mandoc_error.log | python3 ./apply_template.py --rpm_name "{rpm_name}" --file_name "{man_filename}"',
|
||||||
|
shell=True,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
print(f"Error processing file: {file}")
|
||||||
|
with open('/tmp/mandoc_error.log', 'r') as error_log:
|
||||||
|
print(error_log.read())
|
||||||
|
return
|
||||||
|
|
||||||
|
title = ""
|
||||||
|
for line in html_content.splitlines():
|
||||||
|
if '<h1>NAME</h1>' in line:
|
||||||
|
title = line.split('<p>')[1].split('</p>')[0].strip()
|
||||||
|
break
|
||||||
|
title = title or man_filename
|
||||||
|
|
||||||
|
if html_content:
|
||||||
|
with open(output_folder / f"{man_filename}.html", 'w') as f:
|
||||||
|
f.write(html_content)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
for root, _, files in os.walk(MAN_PATH):
|
||||||
|
for file in files:
|
||||||
|
process_file(Path(root) / file)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
46
old_scripts/convert_man.sh
Executable file
46
old_scripts/convert_man.sh
Executable file
@@ -0,0 +1,46 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
ROCKY_VERSION=8.10
|
||||||
|
MAN_PATH=./export/${ROCKY_VERSION}/
|
||||||
|
LOCAL_MAN_PATH=
|
||||||
|
HTML_BASE_PATH=./html_data/${ROCKY_VERSION}/
|
||||||
|
|
||||||
|
process_file() {
|
||||||
|
local file=$1
|
||||||
|
|
||||||
|
local rpm_name
|
||||||
|
rpm_name=$(echo "$file" | cut -d'/' -f 4)
|
||||||
|
local man_context
|
||||||
|
man_context=$(echo "$file" | cut -d'/' -f 8)
|
||||||
|
local man_filename
|
||||||
|
man_filename=$(echo "$file" | awk -F'/' '{print $NF}' | sed -e 's/.gz//g' -e 's/\.[0-9]*$//g')
|
||||||
|
|
||||||
|
local output_folder="${HTML_BASE_PATH}/${rpm_name}/${man_context}/"
|
||||||
|
|
||||||
|
echo "$man_filename"
|
||||||
|
|
||||||
|
mkdir -p "${output_folder}"
|
||||||
|
|
||||||
|
# Try to convert the file and capture any errors
|
||||||
|
# if ! html_content=$(zcat "$file" | groff -Thtml -P-D/dev/null -man 2>/tmp/groff_error.log | pandoc -f html -t html 2>/tmp/pandoc_error.log); then
|
||||||
|
if ! html_content=$(zcat "$file" | mandoc -T html -O fragment 2>/tmp/mandoc_error.log | python3 ./apply_template.py --rpm_name "$rpm_name" --file_name "$man_filename"); then
|
||||||
|
echo "Error processing file: $file"
|
||||||
|
cat /tmp/pandoc_error.log
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local title
|
||||||
|
title=$(echo "$html_content" | sed -n 's/.*<h1>NAME<\/h1>\s*<p>\(.*\)<\/p>/\1/p' | sed 's/<[^>]*>//g')
|
||||||
|
[ -z "$title" ] && title="$man_filename"
|
||||||
|
|
||||||
|
# Check if html_content is empty
|
||||||
|
if [ -n "$html_content" ]; then
|
||||||
|
echo -e "$html_content" > "${output_folder}${man_filename}.html"
|
||||||
|
# echo -e "---\ntitle: \"$title\"\n---\n$html_content" > "${output_folder}${man_filename}.html"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
export -f process_file
|
||||||
|
export HTML_BASE_PATH
|
||||||
|
|
||||||
|
find "$MAN_PATH" -type f | parallel --will-cite process_file
|
||||||
28
old_scripts/extract_man.sh
Executable file
28
old_scripts/extract_man.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#! /bin/bash
|
||||||
|
|
||||||
|
ROCKY_VERSION=8.10
|
||||||
|
MAN_OUTPUT=./export/${ROCKY_VERSION}/
|
||||||
|
DIRECTORY=$1
|
||||||
|
|
||||||
|
if [ -z "$DIRECTORY" ]; then
|
||||||
|
echo "Please provide the directory containing the RPM files"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$MAN_OUTPUT"
|
||||||
|
|
||||||
|
extract_man_pages() {
|
||||||
|
local rpm=$1
|
||||||
|
local man_output=$2
|
||||||
|
|
||||||
|
MANCOUNT=$(rpm2cpio "$rpm" | cpio -itv --quiet | grep -c "/man/")
|
||||||
|
RPMNAME=$(rpm -qp --qf "%{NAME}\n" "$rpm")
|
||||||
|
if [ "$MANCOUNT" -ne 0 ]; then
|
||||||
|
mkdir -p "${man_output}/${RPMNAME}"
|
||||||
|
rpm2cpio "$rpm" | cpio -idmv --quiet -D "${man_output}/${RPMNAME}/" '*/man/*'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
export -f extract_man_pages
|
||||||
|
|
||||||
|
find "$DIRECTORY" -type f -name "*.rpm" | parallel --will-cite -j+0 extract_man_pages {} "$MAN_OUTPUT"
|
||||||
95
old_scripts/generate_index.py
Normal file
95
old_scripts/generate_index.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import gzip
|
||||||
|
from string import Template
|
||||||
|
from collections import defaultdict
|
||||||
|
from fnmatch import fnmatch
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
env = Environment(loader=FileSystemLoader('.'))
|
||||||
|
template = env.get_template('templates/index.j2')
|
||||||
|
|
||||||
|
directory = '/data/html_data' # Change this to your directory path
|
||||||
|
rocky_version = "8.10"
|
||||||
|
|
||||||
|
def generate_sitemap(directory):
|
||||||
|
links = defaultdict(lambda: defaultdict(dict))
|
||||||
|
for root, _, files in os.walk(directory):
|
||||||
|
for file in files:
|
||||||
|
full_filepath = os.path.join(root, file)
|
||||||
|
filepath = full_filepath.split(rocky_version, 1)[-1]
|
||||||
|
|
||||||
|
if any(fnmatch(filepath, pattern) for pattern in ['/index.html', '/links.html','/list.json*', '/sitemap*']):
|
||||||
|
continue
|
||||||
|
|
||||||
|
filepath_parts = filepath.split('/')
|
||||||
|
package_name = filepath_parts[1]
|
||||||
|
man_type = filepath_parts[2]
|
||||||
|
man_type_number = man_type.lstrip('man') if man_type.startswith('man') else man_type
|
||||||
|
command_file = filepath_parts[3]
|
||||||
|
command = command_file.split('.html', 1)[0]
|
||||||
|
|
||||||
|
if filepath.startswith('/'):
|
||||||
|
filepath = filepath[1:]
|
||||||
|
|
||||||
|
fullname = f"{package_name} - {command}({man_type_number})"
|
||||||
|
|
||||||
|
links[package_name][command] = {
|
||||||
|
"url": filepath,
|
||||||
|
"man_type": man_type,
|
||||||
|
"man_type_number": man_type_number,
|
||||||
|
"fullname": fullname
|
||||||
|
}
|
||||||
|
|
||||||
|
return links
|
||||||
|
|
||||||
|
def generate_links_html(links):
|
||||||
|
links_html = ""
|
||||||
|
|
||||||
|
for package_name in links.keys():
|
||||||
|
links_html += f"<h2>package_name</h2>"
|
||||||
|
links_html += "<ul>"
|
||||||
|
for command in links[package_name]:
|
||||||
|
url = links[package_name][command]['url']
|
||||||
|
man_type_number = links[package_name][command]['man_type_number']
|
||||||
|
links_html += f"<li><a href='{url}'>{command}</a>({man_type_number})</li>"
|
||||||
|
links_html += "</ul>"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': f"Rocky Man Page - {rocky_version}",
|
||||||
|
'header_title': f"Rocky Man Page - {rocky_version}",
|
||||||
|
'main_content': f"{links_html}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return template.render(data)
|
||||||
|
|
||||||
|
def convert_sitemap_to_json(links, minify=False):
|
||||||
|
# data
|
||||||
|
# for package_name in links.keys():
|
||||||
|
# for command in links[package_name]:
|
||||||
|
|
||||||
|
# # Add command details to sitemap
|
||||||
|
# sitemap[package_name][command] = {
|
||||||
|
# "url": filepath,
|
||||||
|
# "mantype": man_type,
|
||||||
|
# "fullname": fullname
|
||||||
|
# }
|
||||||
|
|
||||||
|
if minify:
|
||||||
|
return json.dumps(links, separators=(',', ':'))
|
||||||
|
return json.dumps(links, indent=4)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sitemap = generate_sitemap(directory)
|
||||||
|
|
||||||
|
# Output the links HTML page to a file
|
||||||
|
with open(f"{directory}/{rocky_version}/links.html", "w") as file:
|
||||||
|
file.write(generate_links_html(sitemap))
|
||||||
|
|
||||||
|
# Output the list JSON to a file
|
||||||
|
with open(f"{directory}/{rocky_version}/list.json", "w") as file:
|
||||||
|
file.write(convert_sitemap_to_json(sitemap, minify=True))
|
||||||
|
|
||||||
|
# Gzip the JSON file
|
||||||
|
with gzip.open(f"{directory}/{rocky_version}/list.json.gz", "wb") as f_out:
|
||||||
|
f_out.write(convert_sitemap_to_json(sitemap, minify=True).encode('utf-8'))
|
||||||
32
old_scripts/generate_jinja.py
Normal file
32
old_scripts/generate_jinja.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
import os
|
||||||
|
|
||||||
|
env = Environment(loader=FileSystemLoader('.'))
|
||||||
|
template = env.get_template('page.j2')
|
||||||
|
|
||||||
|
# Define the data to pass to the template
|
||||||
|
data = {
|
||||||
|
'title': 'Rocky Man Page - 8.10',
|
||||||
|
'header_title': 'Welcome to Rocky Man Page',
|
||||||
|
'main_content': '<input type="text" id="searchInput" placeholder="Search..."><ul><li>Item 1</li><li>Item 2</li></ul>'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Render the template with the data
|
||||||
|
output = template.render(data)
|
||||||
|
|
||||||
|
print(output)
|
||||||
54
old_scripts/generate_json.py
Normal file
54
old_scripts/generate_json.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import argparse
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
rocky_version = "8.10"
|
||||||
|
|
||||||
|
def create_sitemap(directory):
|
||||||
|
sitemap = defaultdict(lambda: defaultdict(dict))
|
||||||
|
for root, dirs, files in os.walk(directory):
|
||||||
|
for file in files:
|
||||||
|
full_filepath = os.path.join(root, file)
|
||||||
|
filepath = full_filepath.split(rocky_version, 1)[-1]
|
||||||
|
|
||||||
|
# Exclude any path containing 'index.html'
|
||||||
|
if 'index.html' in filepath or 'sitemap.json' in filepath or 'sitemap.xml' in filepath or 'list.json' in filepath or 'list.json.br' in filepath:
|
||||||
|
continue
|
||||||
|
|
||||||
|
filepath_parts = filepath.split('/')
|
||||||
|
package_name = filepath_parts[1]
|
||||||
|
man_type = filepath_parts[2]
|
||||||
|
man_type_number = man_type.lstrip('man') if man_type.startswith('man') else man_type
|
||||||
|
command_file = filepath_parts[3]
|
||||||
|
command = command_file.split('.html', 1)[0]
|
||||||
|
|
||||||
|
if filepath.startswith('/'):
|
||||||
|
filepath = filepath[1:]
|
||||||
|
|
||||||
|
fullname = f"{package_name} - {command}({man_type_number})"
|
||||||
|
|
||||||
|
# Add command details to sitemap
|
||||||
|
sitemap[package_name][command] = {
|
||||||
|
"url": filepath,
|
||||||
|
"mantype": man_type,
|
||||||
|
"fullname": fullname
|
||||||
|
}
|
||||||
|
|
||||||
|
return sitemap
|
||||||
|
|
||||||
|
def convert_sitemap_to_json(sitemap, minify=False):
|
||||||
|
if minify:
|
||||||
|
return json.dumps(sitemap, separators=(',', ':'))
|
||||||
|
return json.dumps(sitemap, indent=4)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(description='Generate sitemap JSON.')
|
||||||
|
parser.add_argument('directory', type=str, help='Directory to scan for HTML files')
|
||||||
|
parser.add_argument('--minify', action='store_true', help='Export minified JSON')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
sitemap = create_sitemap(args.directory)
|
||||||
|
json_output = convert_sitemap_to_json(sitemap, minify=args.minify)
|
||||||
|
|
||||||
|
print(json_output)
|
||||||
135
old_scripts/index_base.html
Normal file
135
old_scripts/index_base.html
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 36 36%22><text y=%2232%22 font-size=%2232%22>🚀</text></svg>">
|
||||||
|
<title>Rocky Man Page - 8.10</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.min.js"></script>
|
||||||
|
<style>
|
||||||
|
/* General Styles */
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #0D0A09;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: large;
|
||||||
|
list-style-type: none;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Styles */
|
||||||
|
.header {
|
||||||
|
background-color: #0FB981;
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Styles */
|
||||||
|
.main-content {
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #282828;
|
||||||
|
color: white;
|
||||||
|
max-width: 800px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content a {
|
||||||
|
color: #0FB981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head-vol {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Adjustments */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.main-content {
|
||||||
|
margin: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
input#searchInput {
|
||||||
|
width: 98%;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
input#searchInput:focus {
|
||||||
|
border-color: #0FB981;
|
||||||
|
box-shadow: 0 0 8px 0 #0FB981;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchInputLabel {
|
||||||
|
display: block;
|
||||||
|
font-size: larger;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header class="header">
|
||||||
|
<h1>Rocky Linux 8.10 - Man Page Listing</h1>
|
||||||
|
</header>
|
||||||
|
<main class="main-content">
|
||||||
|
<label id="searchInputLabel" for="searchInput">Search:</label>
|
||||||
|
<input id="searchInput" placeholder="Loading..." oninput="searchItems()" role="search" disabled />
|
||||||
|
<br />
|
||||||
|
<ul id="results"></ul>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
let fuse;
|
||||||
|
let index;
|
||||||
|
|
||||||
|
fetch('list.json.gz')
|
||||||
|
.then(response => response.body.pipeThrough(new DecompressionStream('gzip')))
|
||||||
|
.then(stream => new Response(stream))
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const flattenedData = [];
|
||||||
|
Object.values(data).forEach(category => {
|
||||||
|
Object.values(category).forEach(item => {
|
||||||
|
flattenedData.push(item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
fuse = new Fuse(flattenedData, {
|
||||||
|
keys: ['fullname'],
|
||||||
|
threshold: 0.2
|
||||||
|
});
|
||||||
|
index = fuse.index; // Create the index
|
||||||
|
document.getElementById("searchInput").placeholder = "";
|
||||||
|
document.getElementById("searchInput").disabled = false;
|
||||||
|
});
|
||||||
|
function searchItems() {
|
||||||
|
const query = document.getElementById("searchInput").value;
|
||||||
|
const results = fuse.search(query, { limit: 50 }); // Limit results for performance
|
||||||
|
const list = document.getElementById("results");
|
||||||
|
list.innerHTML = "";
|
||||||
|
results.forEach(item => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = item.item.url;
|
||||||
|
a.textContent = item.item.fullname;
|
||||||
|
li.appendChild(a);
|
||||||
|
list.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
5
old_scripts/requirements.txt
Normal file
5
old_scripts/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
beautifulsoup4==4.12.3
|
||||||
|
Jinja2==3.1.4
|
||||||
|
MarkupSafe==3.0.2
|
||||||
|
setuptools==68.2.2
|
||||||
|
soupsieve==2.6
|
||||||
362
rocky_man.py
Normal file
362
rocky_man.py
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
import requests
|
||||||
|
import dnf
|
||||||
|
import rpmfile
|
||||||
|
import pprint as pp
|
||||||
|
import gzip
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import tarfile
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from typing import List, Dict, Any, Callable
|
||||||
|
from pathlib import Path
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
sitemap = {}
|
||||||
|
|
||||||
|
class Package:
|
||||||
|
def __init__(self, name: str, repo_type: str, chksum: str, location: str, baseurl: str, license: str, download_path: Path = None, extract_dir: Path = None):
|
||||||
|
self.name = name
|
||||||
|
self.repo_type = repo_type
|
||||||
|
self.chksum = chksum
|
||||||
|
self.location = location
|
||||||
|
self.baseurl = baseurl
|
||||||
|
self.filename = location.split("/")[-1]
|
||||||
|
self.license = license
|
||||||
|
self.download_path = download_path
|
||||||
|
self.extract_dir = extract_dir
|
||||||
|
|
||||||
|
class ManFile:
|
||||||
|
def __init__(self, filelocation: Path):
|
||||||
|
self.filelocation = filelocation
|
||||||
|
self.filename = self.filelocation.parts[-1]
|
||||||
|
self.context = self.filelocation.parts[-2]
|
||||||
|
self.context_number = str(''.join(filter(str.isdigit, self.context)))
|
||||||
|
self.regular_name = self.filename.replace(".gz","")
|
||||||
|
self.name = ".".join(self.regular_name.split(".")[:-1])
|
||||||
|
self.man_text = None
|
||||||
|
self.man_html = None
|
||||||
|
self.generated_html = None
|
||||||
|
self.html_folder_location = None
|
||||||
|
self._html_file_location = None
|
||||||
|
self.html_uri_location = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def html_file_location(self):
|
||||||
|
return self._html_file_location
|
||||||
|
|
||||||
|
@html_file_location.setter
|
||||||
|
def html_file_location(self, value: Path):
|
||||||
|
self._html_file_location = value
|
||||||
|
if value:
|
||||||
|
self.html_uri_location = "/".join(value.parts[2:])
|
||||||
|
else:
|
||||||
|
self.html_uri_location = ""
|
||||||
|
|
||||||
|
class ManMaker:
|
||||||
|
def __init__(self, man_dir: str, html_dir: str):
|
||||||
|
self.man_dir = man_dir
|
||||||
|
self.html_dir = html_dir
|
||||||
|
|
||||||
|
def zcat(self, file_path: Path):
|
||||||
|
with gzip.open(file_path, 'rb') as f:
|
||||||
|
file_content = f.read()
|
||||||
|
return file_content.decode('utf-8')
|
||||||
|
|
||||||
|
def extract_man_files(self, package: Package):
|
||||||
|
rpm_file = package.download_path.stem
|
||||||
|
|
||||||
|
extract_dir = Path(f"{self.man_dir}/{rpm_file}")
|
||||||
|
extract_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
package.extract_dir = extract_dir
|
||||||
|
|
||||||
|
man_files = []
|
||||||
|
with rpmfile.open(package.download_path) as rpm:
|
||||||
|
for member in rpm.getmembers():
|
||||||
|
if "/man/" in member.name:
|
||||||
|
man_file = ManFile(filelocation=extract_dir / member.name)
|
||||||
|
man_file.filelocation.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with open(man_file.filelocation, "wb") as f:
|
||||||
|
f.write(rpm.extractfile(member).read())
|
||||||
|
man_files.append(man_file)
|
||||||
|
|
||||||
|
self.get_man_file_contents(package, man_files)
|
||||||
|
|
||||||
|
def get_man_file_contents(self, package: Package, man_files: List[ManFile]):
|
||||||
|
for man_file in man_files:
|
||||||
|
try:
|
||||||
|
man_file.man_text = self.zcat(man_file.filelocation)
|
||||||
|
self.convert_man_to_html(man_file, package)
|
||||||
|
except gzip.BadGzipFile as e:
|
||||||
|
# print(f"{e}: {man_file.filelocation}")
|
||||||
|
pass
|
||||||
|
|
||||||
|
def convert_man_to_html(self, man_file: ManFile, package: Package):
|
||||||
|
process = subprocess.Popen(
|
||||||
|
['mandoc', '-T', 'html', '-O', 'fragment,toc'],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
man_file.man_html, stderr = process.communicate(input=man_file.man_text)
|
||||||
|
if process.returncode != 0:
|
||||||
|
print(f"Error converting man to HTML: {stderr}")
|
||||||
|
else:
|
||||||
|
self.clean_html(man_file, package)
|
||||||
|
|
||||||
|
def clean_html(self, man_file: ManFile, package: Package):
|
||||||
|
man_file.man_html = re.sub(r'<td class="head-ltitle">\(\)</td>', '<td class="head-ltitle"></td>', man_file.man_html)
|
||||||
|
man_file.man_html = re.sub(r'<td class="head-rtitle">\(\)</td>', '<td class="head-rtitle"></td>', man_file.man_html)
|
||||||
|
man_file.man_html.strip()
|
||||||
|
self.generate_html(man_file, package)
|
||||||
|
|
||||||
|
def clean_name(self, man_file: ManFile):
|
||||||
|
invalid_filenames = {
|
||||||
|
"..1": "..1".replace("..", "__"),
|
||||||
|
":.1": ":.1".replace(":.", "_"),
|
||||||
|
"[.1": "[.1".replace("[", "(").replace(".", "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
cleaned_name = man_file.regular_name
|
||||||
|
if cleaned_name in invalid_filenames:
|
||||||
|
cleaned_name = invalid_filenames[cleaned_name]
|
||||||
|
|
||||||
|
return cleaned_name
|
||||||
|
|
||||||
|
def generate_html(self, man_file: ManFile, package: Package):
|
||||||
|
env = setup_jinja()
|
||||||
|
template = env.get_template("man_page.j2")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': f'{man_file.name} - {package.name} - Rocky Man Page',
|
||||||
|
'header_title': f'{man_file.name}',
|
||||||
|
'main_content': man_file.man_html
|
||||||
|
}
|
||||||
|
|
||||||
|
man_file.generated_html = template.render(data)
|
||||||
|
self.save_html(man_file, package)
|
||||||
|
|
||||||
|
def save_html(self, man_file: ManFile, package: Package):
|
||||||
|
man_file.html_folder_location = html_folder_export(man_file, package, self.html_dir)
|
||||||
|
man_file.html_folder_location.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
man_file.html_file_location = man_file.html_folder_location / f"{self.clean_name(man_file)}.html"
|
||||||
|
|
||||||
|
with open(man_file.html_file_location, "w") as f:
|
||||||
|
f.write(man_file.generated_html)
|
||||||
|
# print(f"Saved HTML to {man_file.html_file_location}")
|
||||||
|
|
||||||
|
self.update_sitemap(man_file, package)
|
||||||
|
|
||||||
|
def update_sitemap(self, man_file: ManFile, package: Package):
|
||||||
|
global sitemap
|
||||||
|
if package.name not in sitemap:
|
||||||
|
sitemap[package.name] = {}
|
||||||
|
sitemap[package.name][man_file.name] = {
|
||||||
|
"url": str(man_file.html_uri_location),
|
||||||
|
"man_type": man_file.context,
|
||||||
|
"man_type_number": man_file.context_number,
|
||||||
|
"repo_type": package.repo_type,
|
||||||
|
"fullname": f"{package.name} - {man_file.name}({man_file.context_number})"
|
||||||
|
}
|
||||||
|
|
||||||
|
class RepoManager:
|
||||||
|
def __init__(self, base_url: str, contentdir: str, releasever: str, basearch: str, repo_type: str, download_dir, enabled: bool = True, gpgcheck: bool = False):
|
||||||
|
self.base_url = base_url
|
||||||
|
self.contentdir = contentdir
|
||||||
|
self.releasever = releasever
|
||||||
|
self.basearch = basearch
|
||||||
|
self.repo_type = repo_type
|
||||||
|
self.repo_name = f"{repo_type}-{releasever}"
|
||||||
|
|
||||||
|
self.download_dir = download_dir
|
||||||
|
|
||||||
|
self.enabled = enabled
|
||||||
|
self.gpgcheck = gpgcheck
|
||||||
|
|
||||||
|
self.base = dnf.Base()
|
||||||
|
self.base.conf.debuglevel = 0
|
||||||
|
self.base.conf.errorlevel = 0
|
||||||
|
|
||||||
|
self.download_dir = Path(download_dir)
|
||||||
|
self.download_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._configure_repo()
|
||||||
|
|
||||||
|
def generate_repo_url(self):
|
||||||
|
repo_url = urljoin(self.base_url, f"{self.contentdir}/{self.releasever}/BaseOS/{self.basearch}/os/")
|
||||||
|
return repo_url
|
||||||
|
|
||||||
|
def print_repo_url(self):
|
||||||
|
repo_url = self.generate_repo_url()
|
||||||
|
print(f"Repository URL: {repo_url}")
|
||||||
|
|
||||||
|
def _configure_repo(self):
|
||||||
|
repo = dnf.repo.Repo(self.repo_name, self.base.conf)
|
||||||
|
repo_url = self.generate_repo_url()
|
||||||
|
repo.baseurl = [repo_url]
|
||||||
|
repo.enabled = self.enabled
|
||||||
|
repo.gpgcheck = self.gpgcheck
|
||||||
|
self.base.repos.add(repo)
|
||||||
|
self.base.fill_sack(load_system_repo=False)
|
||||||
|
|
||||||
|
def print_repo(self):
|
||||||
|
repo = self.base.repos
|
||||||
|
print(repo)
|
||||||
|
|
||||||
|
def list_packages(self) -> List[str]:
|
||||||
|
package_list = []
|
||||||
|
for pkg in self.base.sack.query().available():
|
||||||
|
package_list.append(pkg.name)
|
||||||
|
return package_list
|
||||||
|
|
||||||
|
def list_packages_raw(self):
|
||||||
|
for pkg in self.base.sack.query().available():
|
||||||
|
print(f"Package: {pkg.name}")
|
||||||
|
for attr in dir(pkg):
|
||||||
|
if not attr.startswith("_"):
|
||||||
|
print(f" {attr}: {getattr(pkg, attr)}")
|
||||||
|
print("\n")
|
||||||
|
break
|
||||||
|
|
||||||
|
def list_package_object(self, package_name: str) -> List[Package]:
|
||||||
|
pkgs = self.base.sack.query().filter(name=package_name)
|
||||||
|
|
||||||
|
if not pkgs:
|
||||||
|
raise ValueError(f"Package {package_name} not found in the repository.")
|
||||||
|
|
||||||
|
return self.generate_package_list(pkgs)
|
||||||
|
|
||||||
|
def list_packages_object(self) -> List[Package]:
|
||||||
|
pkgs = self.base.sack.query().available()
|
||||||
|
|
||||||
|
if not pkgs:
|
||||||
|
raise ValueError(f"No packages found in the repository.")
|
||||||
|
|
||||||
|
return self.generate_package_list(pkgs)
|
||||||
|
|
||||||
|
def generate_package_list(self, pkgs) -> List[Package]:
|
||||||
|
package_list = []
|
||||||
|
for pkg in pkgs:
|
||||||
|
repo = pkg.repo
|
||||||
|
package_info = Package(
|
||||||
|
name=getattr(pkg, "name", None),
|
||||||
|
repo_type=self.repo_type,
|
||||||
|
chksum=getattr(pkg, "chksum", None),
|
||||||
|
location=getattr(pkg, "location", None),
|
||||||
|
baseurl=repo.baseurl[0] if repo and repo.baseurl else None,
|
||||||
|
license=getattr(pkg, "license", None)
|
||||||
|
)
|
||||||
|
package_list.append(package_info)
|
||||||
|
return package_list
|
||||||
|
|
||||||
|
def download_file(self, download_url: str, download_path: Path):
|
||||||
|
if download_path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
response = requests.get(download_url)
|
||||||
|
response.raise_for_status()
|
||||||
|
with open(download_path, "wb") as f:
|
||||||
|
f.write(response.content)
|
||||||
|
|
||||||
|
def download_package(self, package_name: str, man_maker: ManMaker) -> Package:
|
||||||
|
packages = self.list_package_object(package_name)
|
||||||
|
|
||||||
|
for package in packages:
|
||||||
|
download_url = urljoin(package.baseurl, package.location)
|
||||||
|
download_path = self.download_dir / f"{package.filename}"
|
||||||
|
package.download_path = download_path
|
||||||
|
self.download_file(download_url, download_path)
|
||||||
|
|
||||||
|
# Process the package immediately after downloading
|
||||||
|
man_maker.extract_man_files(package)
|
||||||
|
|
||||||
|
return package
|
||||||
|
|
||||||
|
def download_all_packages(self, man_maker: ManMaker) -> List[Package]:
|
||||||
|
packages = self.list_packages_object()
|
||||||
|
downloaded_files = []
|
||||||
|
|
||||||
|
for package in packages:
|
||||||
|
try:
|
||||||
|
downloaded_files.append(self.download_package(package.name, man_maker))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error downloading package: {e}")
|
||||||
|
|
||||||
|
return downloaded_files
|
||||||
|
|
||||||
|
def delete_package(self, rpm_path: Path):
|
||||||
|
rpm_path.unlink()
|
||||||
|
|
||||||
|
def save_json(sitemap: Dict[str, Dict[str, Any]], json_file_location: Path):
|
||||||
|
sorted_sitemap = {k: sitemap[k] for k in sorted(sitemap)}
|
||||||
|
|
||||||
|
# Save the JSON file
|
||||||
|
with open(json_file_location, "w") as f:
|
||||||
|
json.dump(sorted_sitemap, f)
|
||||||
|
|
||||||
|
# Save the gzipped JSON file
|
||||||
|
gzipped_file_location = f"{json_file_location}.gz"
|
||||||
|
with gzip.open(gzipped_file_location, "wt") as gz:
|
||||||
|
json.dump(sorted_sitemap, gz)
|
||||||
|
|
||||||
|
def html_folder_export(man_file: ManFile, package: Package, html_base_dir: str) -> Path:
|
||||||
|
return Path(f"{html_base_dir}/{package.name}/{man_file.context}")
|
||||||
|
|
||||||
|
def setup_jinja():
|
||||||
|
env = Environment(loader=FileSystemLoader('./templates'))
|
||||||
|
return env
|
||||||
|
|
||||||
|
def generate_index(releasever: str, html_dir: str):
|
||||||
|
env = setup_jinja()
|
||||||
|
template = env.get_template("index.j2")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': f'Rocky Linux {releasever} - Man Page Search',
|
||||||
|
'header_title': f'Rocky Linux {releasever} - Man Page Search'
|
||||||
|
}
|
||||||
|
|
||||||
|
render = template.render(data)
|
||||||
|
with open(f"{html_dir}/index.html", "w") as f:
|
||||||
|
f.write(render)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
BASE_URL = "http://dl.rockylinux.org/"
|
||||||
|
CONTENTDIR = "pub/rocky"
|
||||||
|
RELEASEVERS = ["8.10", "9.5"]
|
||||||
|
BASEARCH = "aarch64"
|
||||||
|
REPO_TYPES = ["BaseOS", "AppStream"]
|
||||||
|
DOWNLOAD_BASE_DIR = "./tmp/repo"
|
||||||
|
MAN_BASE_DIR = "./tmp/export"
|
||||||
|
HTML_BASE_DIR = "./html"
|
||||||
|
|
||||||
|
for RELEASEVER in RELEASEVERS:
|
||||||
|
for REPO_TYPE in REPO_TYPES:
|
||||||
|
DOWNLOAD_DIR = f"{DOWNLOAD_BASE_DIR}/{RELEASEVER}/{REPO_TYPE}"
|
||||||
|
MAN_DIR = f"{MAN_BASE_DIR}/{RELEASEVER}/{REPO_TYPE}"
|
||||||
|
HTML_DIR = f"{HTML_BASE_DIR}/{RELEASEVER}"
|
||||||
|
|
||||||
|
repo_manager = RepoManager(
|
||||||
|
base_url = BASE_URL,
|
||||||
|
contentdir = CONTENTDIR,
|
||||||
|
releasever = RELEASEVER,
|
||||||
|
basearch = BASEARCH,
|
||||||
|
repo_type = REPO_TYPE,
|
||||||
|
download_dir = DOWNLOAD_DIR,
|
||||||
|
enabled = True,
|
||||||
|
gpgcheck = False
|
||||||
|
)
|
||||||
|
|
||||||
|
man_maker = ManMaker(man_dir=MAN_DIR, html_dir=HTML_DIR)
|
||||||
|
|
||||||
|
print("Downloading packages and generating HTML...")
|
||||||
|
repo_manager.download_all_packages(man_maker)
|
||||||
|
# repo_manager.download_package("at", man_maker)
|
||||||
|
|
||||||
|
generate_index(RELEASEVER, HTML_DIR)
|
||||||
|
save_json(sitemap, Path(f"{HTML_DIR}/list.json"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
381
rocky_man2.py
Normal file
381
rocky_man2.py
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import aiofiles
|
||||||
|
import dnf
|
||||||
|
import rpmfile
|
||||||
|
import pprint as pp
|
||||||
|
import gzip
|
||||||
|
import subprocess
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import tarfile
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
from typing import List, Dict, Any, Callable
|
||||||
|
from pathlib import Path
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
sitemap = {}
|
||||||
|
|
||||||
|
class Package:
|
||||||
|
def __init__(self, name: str, repo_type: str, chksum: str, location: str, baseurl: str, license: str, download_path: Path = None, extract_dir: Path = None):
|
||||||
|
self.name = name
|
||||||
|
self.repo_type = repo_type
|
||||||
|
self.chksum = chksum
|
||||||
|
self.location = location
|
||||||
|
self.baseurl = baseurl
|
||||||
|
self.filename = location.split("/")[-1]
|
||||||
|
self.license = license
|
||||||
|
self.download_path = download_path
|
||||||
|
self.extract_dir = extract_dir
|
||||||
|
|
||||||
|
class ManFile:
|
||||||
|
def __init__(self, filelocation: Path):
|
||||||
|
self.filelocation = filelocation
|
||||||
|
self.filename = self.filelocation.parts[-1]
|
||||||
|
self.context = self.filelocation.parts[-2]
|
||||||
|
self.context_number = str(''.join(filter(str.isdigit, self.context)))
|
||||||
|
self.regular_name = self.filename.replace(".gz","")
|
||||||
|
self.name = ".".join(self.regular_name.split(".")[:-1])
|
||||||
|
self.man_text = None
|
||||||
|
self.man_html = None
|
||||||
|
self.generated_html = None
|
||||||
|
self.html_folder_location = None
|
||||||
|
self._html_file_location = None
|
||||||
|
self.html_uri_location = ""
|
||||||
|
|
||||||
|
@property
|
||||||
|
def html_file_location(self):
|
||||||
|
return self._html_file_location
|
||||||
|
|
||||||
|
@html_file_location.setter
|
||||||
|
def html_file_location(self, value: Path):
|
||||||
|
self._html_file_location = value
|
||||||
|
if value:
|
||||||
|
self.html_uri_location = "/".join(value.parts[2:])
|
||||||
|
else:
|
||||||
|
self.html_uri_location = ""
|
||||||
|
|
||||||
|
class ManMaker:
|
||||||
|
def __init__(self, man_dir: str, html_dir: str):
|
||||||
|
self.man_dir = man_dir
|
||||||
|
self.html_dir = html_dir
|
||||||
|
|
||||||
|
async def zcat(self, file_path: Path):
|
||||||
|
async with aiofiles.open(file_path, 'rb') as f:
|
||||||
|
content = await f.read()
|
||||||
|
try:
|
||||||
|
return gzip.decompress(content).decode('utf-8')
|
||||||
|
except gzip.BadGzipFile:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def extract_man_files(self, package: Package):
|
||||||
|
rpm_file = package.download_path.stem
|
||||||
|
|
||||||
|
extract_dir = Path(f"{self.man_dir}/{rpm_file}")
|
||||||
|
extract_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
package.extract_dir = extract_dir
|
||||||
|
|
||||||
|
man_files = []
|
||||||
|
with rpmfile.open(package.download_path) as rpm:
|
||||||
|
for member in rpm.getmembers():
|
||||||
|
if "/man/" in member.name:
|
||||||
|
man_file = ManFile(filelocation=extract_dir / member.name)
|
||||||
|
man_file.filelocation.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
async with aiofiles.open(man_file.filelocation, "wb") as f:
|
||||||
|
await f.write(rpm.extractfile(member).read())
|
||||||
|
man_files.append(man_file)
|
||||||
|
|
||||||
|
await self.get_man_file_contents(package, man_files)
|
||||||
|
|
||||||
|
async def get_man_file_contents(self, package: Package, man_files: List[ManFile]):
|
||||||
|
tasks = [self.process_man_file(man_file, package) for man_file in man_files]
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
async def process_man_file(self, man_file: ManFile, package: Package):
|
||||||
|
try:
|
||||||
|
man_file.man_text = await self.zcat(man_file.filelocation)
|
||||||
|
if man_file.man_text:
|
||||||
|
await self.convert_man_to_html(man_file, package)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing {man_file.filelocation}: {e}")
|
||||||
|
|
||||||
|
async def convert_man_to_html(self, man_file: ManFile, package: Package):
|
||||||
|
process = await asyncio.create_subprocess_exec(
|
||||||
|
'mandoc', '-T', 'html', '-O', 'fragment,toc',
|
||||||
|
stdin=asyncio.subprocess.PIPE,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout, stderr = await process.communicate(input=man_file.man_text.encode())
|
||||||
|
man_file.man_html = stdout.decode()
|
||||||
|
|
||||||
|
if process.returncode == 0:
|
||||||
|
await self.clean_html(man_file, package)
|
||||||
|
else:
|
||||||
|
print(f"Error converting man to HTML: {stderr.decode()}")
|
||||||
|
|
||||||
|
async def clean_html(self, man_file: ManFile, package: Package):
|
||||||
|
man_file.man_html = re.sub(r'<td class="head-ltitle">\(\)</td>', '<td class="head-ltitle"></td>', man_file.man_html)
|
||||||
|
man_file.man_html = re.sub(r'<td class="head-rtitle">\(\)</td>', '<td class="head-rtitle"></td>', man_file.man_html)
|
||||||
|
man_file.man_html.strip()
|
||||||
|
await self.generate_html(man_file, package)
|
||||||
|
|
||||||
|
def clean_name(self, man_file: ManFile):
|
||||||
|
invalid_filenames = {
|
||||||
|
"..1": "..1".replace("..", "__"),
|
||||||
|
":.1": ":.1".replace(":.", "_"),
|
||||||
|
"[.1": "[.1".replace("[", "(").replace(".", "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
cleaned_name = man_file.regular_name
|
||||||
|
if cleaned_name in invalid_filenames:
|
||||||
|
cleaned_name = invalid_filenames[cleaned_name]
|
||||||
|
|
||||||
|
return cleaned_name
|
||||||
|
|
||||||
|
async def generate_html(self, man_file: ManFile, package: Package):
|
||||||
|
env = setup_jinja()
|
||||||
|
template = env.get_template("man_page.j2")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': f'{man_file.name} - {package.name} - Rocky Man Page',
|
||||||
|
'header_title': f'{man_file.name}',
|
||||||
|
'main_content': man_file.man_html
|
||||||
|
}
|
||||||
|
|
||||||
|
man_file.generated_html = template.render(data)
|
||||||
|
await self.save_html(man_file, package)
|
||||||
|
|
||||||
|
async def save_html(self, man_file: ManFile, package: Package):
|
||||||
|
man_file.html_folder_location = html_folder_export(man_file, package, self.html_dir)
|
||||||
|
man_file.html_folder_location.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
man_file.html_file_location = man_file.html_folder_location / f"{self.clean_name(man_file)}.html"
|
||||||
|
|
||||||
|
async with aiofiles.open(man_file.html_file_location, "w") as f:
|
||||||
|
await f.write(man_file.generated_html)
|
||||||
|
|
||||||
|
self.update_sitemap(man_file, package)
|
||||||
|
|
||||||
|
def update_sitemap(self, man_file: ManFile, package: Package):
|
||||||
|
global sitemap
|
||||||
|
if package.name not in sitemap:
|
||||||
|
sitemap[package.name] = {}
|
||||||
|
sitemap[package.name][man_file.name] = {
|
||||||
|
"url": str(man_file.html_uri_location),
|
||||||
|
"man_type": man_file.context,
|
||||||
|
"man_type_number": man_file.context_number,
|
||||||
|
"repo_type": package.repo_type,
|
||||||
|
"fullname": f"{package.name} - {man_file.name}({man_file.context_number})"
|
||||||
|
}
|
||||||
|
|
||||||
|
class RepoManager:
|
||||||
|
def __init__(self, base_url: str, contentdir: str, releasever: str, basearch: str, repo_type: str, download_dir, enabled: bool = True, gpgcheck: bool = False):
|
||||||
|
self.base_url = base_url
|
||||||
|
self.contentdir = contentdir
|
||||||
|
self.releasever = releasever
|
||||||
|
self.basearch = basearch
|
||||||
|
self.repo_type = repo_type
|
||||||
|
self.repo_name = f"{repo_type}-{releasever}"
|
||||||
|
|
||||||
|
self.download_dir = download_dir
|
||||||
|
|
||||||
|
self.enabled = enabled
|
||||||
|
self.gpgcheck = gpgcheck
|
||||||
|
|
||||||
|
self.base = dnf.Base()
|
||||||
|
self.base.conf.debuglevel = 0
|
||||||
|
self.base.conf.errorlevel = 0
|
||||||
|
|
||||||
|
self.download_dir = Path(download_dir)
|
||||||
|
self.download_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._configure_repo()
|
||||||
|
self.session = None
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
self.session = aiohttp.ClientSession()
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
if self.session:
|
||||||
|
await self.session.close()
|
||||||
|
|
||||||
|
def generate_repo_url(self):
|
||||||
|
repo_url = urljoin(self.base_url, f"{self.contentdir}/{self.releasever}/BaseOS/{self.basearch}/os/")
|
||||||
|
return repo_url
|
||||||
|
|
||||||
|
def print_repo_url(self):
|
||||||
|
repo_url = self.generate_repo_url()
|
||||||
|
print(f"Repository URL: {repo_url}")
|
||||||
|
|
||||||
|
def _configure_repo(self):
|
||||||
|
repo = dnf.repo.Repo(self.repo_name, self.base.conf)
|
||||||
|
repo_url = self.generate_repo_url()
|
||||||
|
repo.baseurl = [repo_url]
|
||||||
|
repo.enabled = self.enabled
|
||||||
|
repo.gpgcheck = self.gpgcheck
|
||||||
|
self.base.repos.add(repo)
|
||||||
|
self.base.fill_sack(load_system_repo=False)
|
||||||
|
|
||||||
|
def print_repo(self):
|
||||||
|
repo = self.base.repos
|
||||||
|
print(repo)
|
||||||
|
|
||||||
|
def list_packages(self) -> List[str]:
|
||||||
|
package_list = []
|
||||||
|
for pkg in self.base.sack.query().available():
|
||||||
|
package_list.append(pkg.name)
|
||||||
|
return package_list
|
||||||
|
|
||||||
|
def list_packages_raw(self):
|
||||||
|
for pkg in self.base.sack.query().available():
|
||||||
|
print(f"Package: {pkg.name}")
|
||||||
|
for attr in dir(pkg):
|
||||||
|
if not attr.startswith("_"):
|
||||||
|
print(f" {attr}: {getattr(pkg, attr)}")
|
||||||
|
print("\n")
|
||||||
|
break
|
||||||
|
|
||||||
|
def list_package_object(self, package_name: str) -> List[Package]:
|
||||||
|
pkgs = self.base.sack.query().filter(name=package_name)
|
||||||
|
|
||||||
|
if not pkgs:
|
||||||
|
raise ValueError(f"Package {package_name} not found in the repository.")
|
||||||
|
|
||||||
|
return self.generate_package_list(pkgs)
|
||||||
|
|
||||||
|
def list_packages_object(self) -> List[Package]:
|
||||||
|
pkgs = self.base.sack.query().available()
|
||||||
|
|
||||||
|
if not pkgs:
|
||||||
|
raise ValueError(f"No packages found in the repository.")
|
||||||
|
|
||||||
|
return self.generate_package_list(pkgs)
|
||||||
|
|
||||||
|
def generate_package_list(self, pkgs) -> List[Package]:
|
||||||
|
package_list = []
|
||||||
|
for pkg in pkgs:
|
||||||
|
repo = pkg.repo
|
||||||
|
package_info = Package(
|
||||||
|
name=getattr(pkg, "name", None),
|
||||||
|
repo_type=self.repo_type,
|
||||||
|
chksum=getattr(pkg, "chksum", None),
|
||||||
|
location=getattr(pkg, "location", None),
|
||||||
|
baseurl=repo.baseurl[0] if repo and repo.baseurl else None,
|
||||||
|
license=getattr(pkg, "license", None)
|
||||||
|
)
|
||||||
|
package_list.append(package_info)
|
||||||
|
return package_list
|
||||||
|
|
||||||
|
async def download_file(self, download_url: str, download_path: Path):
|
||||||
|
if download_path.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
async with self.session.get(download_url) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
async with aiofiles.open(download_path, "wb") as f:
|
||||||
|
await f.write(await response.read())
|
||||||
|
|
||||||
|
async def download_package(self, package_name: str, man_maker: ManMaker) -> Package:
|
||||||
|
packages = self.list_package_object(package_name)
|
||||||
|
|
||||||
|
for package in packages:
|
||||||
|
download_url = urljoin(package.baseurl, package.location)
|
||||||
|
download_path = self.download_dir / f"{package.filename}"
|
||||||
|
package.download_path = download_path
|
||||||
|
await self.download_file(download_url, download_path)
|
||||||
|
|
||||||
|
await man_maker.extract_man_files(package)
|
||||||
|
|
||||||
|
return package
|
||||||
|
|
||||||
|
async def download_all_packages(self, man_maker: ManMaker) -> List[Package]:
|
||||||
|
packages = self.list_packages_object()
|
||||||
|
tasks = []
|
||||||
|
|
||||||
|
for package in packages:
|
||||||
|
try:
|
||||||
|
tasks.append(self.download_package(package.name, man_maker))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error queueing package: {e}")
|
||||||
|
|
||||||
|
return await asyncio.gather(*tasks)
|
||||||
|
|
||||||
|
def delete_package(self, rpm_path: Path):
|
||||||
|
rpm_path.unlink()
|
||||||
|
|
||||||
|
async def save_json(sitemap: Dict[str, Dict[str, Any]], json_file_location: Path):
|
||||||
|
sorted_sitemap = {k: sitemap[k] for k in sorted(sitemap)}
|
||||||
|
|
||||||
|
async with aiofiles.open(json_file_location, "w") as f:
|
||||||
|
await f.write(json.dumps(sorted_sitemap))
|
||||||
|
|
||||||
|
gzipped_file_location = f"{json_file_location}.gz"
|
||||||
|
with gzip.open(gzipped_file_location, "wt") as gz:
|
||||||
|
json.dump(sorted_sitemap, gz)
|
||||||
|
|
||||||
|
def html_folder_export(man_file: ManFile, package: Package, html_base_dir: str) -> Path:
|
||||||
|
return Path(f"{html_base_dir}/{package.name}/{man_file.context}")
|
||||||
|
|
||||||
|
def setup_jinja():
|
||||||
|
env = Environment(loader=FileSystemLoader('./templates'))
|
||||||
|
return env
|
||||||
|
|
||||||
|
async def generate_index(releasever: str, html_dir: str):
|
||||||
|
env = setup_jinja()
|
||||||
|
template = env.get_template("index.j2")
|
||||||
|
|
||||||
|
data = {
|
||||||
|
'title': f'Rocky Linux {releasever} - Man Page Search',
|
||||||
|
'header_title': f'Rocky Linux {releasever} - Man Page Search'
|
||||||
|
}
|
||||||
|
|
||||||
|
render = template.render(data)
|
||||||
|
async with aiofiles.open(f"{html_dir}/index.html", "w") as f:
|
||||||
|
await f.write(render)
|
||||||
|
|
||||||
|
async def process_repo(base_url: str, contentdir: str, releasever: str, basearch: str,
|
||||||
|
repo_type: str, download_dir: str, man_dir: str, html_dir: str):
|
||||||
|
async with RepoManager(
|
||||||
|
base_url=base_url,
|
||||||
|
contentdir=contentdir,
|
||||||
|
releasever=releasever,
|
||||||
|
basearch=basearch,
|
||||||
|
repo_type=repo_type,
|
||||||
|
download_dir=download_dir,
|
||||||
|
enabled=True,
|
||||||
|
gpgcheck=False
|
||||||
|
) as repo_manager:
|
||||||
|
man_maker = ManMaker(man_dir=man_dir, html_dir=html_dir)
|
||||||
|
print(f"Processing {repo_type} for {releasever}...")
|
||||||
|
await repo_manager.download_all_packages(man_maker)
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
BASE_URL = "https://ord.mirror.rackspace.com/"
|
||||||
|
CONTENTDIR = "rocky"
|
||||||
|
RELEASEVERS = ["8.10", "9.5"]
|
||||||
|
BASEARCH = "aarch64"
|
||||||
|
REPO_TYPES = ["BaseOS", "AppStream"]
|
||||||
|
DOWNLOAD_BASE_DIR = "./tmp/repo"
|
||||||
|
MAN_BASE_DIR = "./tmp/export"
|
||||||
|
HTML_BASE_DIR = "./html"
|
||||||
|
|
||||||
|
for RELEASEVER in RELEASEVERS:
|
||||||
|
tasks = []
|
||||||
|
for REPO_TYPE in REPO_TYPES:
|
||||||
|
DOWNLOAD_DIR = f"{DOWNLOAD_BASE_DIR}/{RELEASEVER}/{REPO_TYPE}"
|
||||||
|
MAN_DIR = f"{MAN_BASE_DIR}/{RELEASEVER}/{REPO_TYPE}"
|
||||||
|
HTML_DIR = f"{HTML_BASE_DIR}/{RELEASEVER}"
|
||||||
|
|
||||||
|
tasks.append(process_repo(
|
||||||
|
BASE_URL, CONTENTDIR, RELEASEVER, BASEARCH,
|
||||||
|
REPO_TYPE, DOWNLOAD_DIR, MAN_DIR, HTML_DIR
|
||||||
|
))
|
||||||
|
|
||||||
|
await asyncio.gather(*tasks)
|
||||||
|
await generate_index(RELEASEVER, HTML_DIR)
|
||||||
|
await save_json(sitemap, Path(f"{HTML_DIR}/list.json"))
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
80
templates/base.j2
Normal file
80
templates/base.j2
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<link rel="icon"
|
||||||
|
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 36 36%22><text y=%2232%22 font-size=%2232%22>🚀</text></svg>">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.min.js"></script>
|
||||||
|
<style>
|
||||||
|
/* Reset Styles */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* General Styles */
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #0D0A09;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
font-size: large;
|
||||||
|
list-style-type: none;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Styles */
|
||||||
|
.header {
|
||||||
|
background-color: #0FB981;
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Content Styles */
|
||||||
|
.main-content {
|
||||||
|
margin: 2rem auto;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: #282828;
|
||||||
|
color: white;
|
||||||
|
max-width: 800px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content a {
|
||||||
|
color: #0FB981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.head-vol {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Bl-compact { #Table of Contents
|
||||||
|
list-style-type: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Adjustments */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.main-content {
|
||||||
|
margin: 1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Extra CSS */
|
||||||
|
{% block extra_css %}
|
||||||
|
{% endblock %}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{% block body %}
|
||||||
|
{% endblock %}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
78
templates/index.j2
Normal file
78
templates/index.j2
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{% extends "base.j2" %}
|
||||||
|
{% block extra_css %}
|
||||||
|
input#searchInput {
|
||||||
|
width: 100%;
|
||||||
|
height: 2rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
input#searchInput:focus {
|
||||||
|
border-color: #0FB981;
|
||||||
|
box-shadow: 0 0 8px 0 #0FB981;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchInputLabel {
|
||||||
|
display: block;
|
||||||
|
font-size: larger;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
{% endblock %}
|
||||||
|
{% block body %}
|
||||||
|
<header class="header">
|
||||||
|
<h1>{{ header_title }}</h1>
|
||||||
|
</header>
|
||||||
|
<main class="main-content">
|
||||||
|
<label id="searchInputLabel" for="searchInput">Search:</label>
|
||||||
|
<input id="searchInput" placeholder="Loading..." oninput="searchItems()" role="search" disabled />
|
||||||
|
<br />
|
||||||
|
<h2 id="result_header"></h2>
|
||||||
|
<ul id="results"></ul>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
let fuse;
|
||||||
|
let index;
|
||||||
|
|
||||||
|
fetch('list.json.gz')
|
||||||
|
.then(response => response.body.pipeThrough(new DecompressionStream('gzip')))
|
||||||
|
.then(stream => new Response(stream))
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
const flattenedData = [];
|
||||||
|
Object.values(data).forEach(category => {
|
||||||
|
Object.values(category).forEach(item => {
|
||||||
|
flattenedData.push(item);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
fuse = new Fuse(flattenedData, {
|
||||||
|
keys: ['fullname'],
|
||||||
|
threshold: 0.2
|
||||||
|
});
|
||||||
|
index = fuse.index; // Create the index
|
||||||
|
document.getElementById("searchInput").placeholder = "";
|
||||||
|
document.getElementById("searchInput").disabled = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
function searchItems() {
|
||||||
|
const query = document.getElementById("searchInput").value;
|
||||||
|
const results = fuse.search(query, { limit: 50 }); // Limit results for performance
|
||||||
|
const list = document.getElementById("results");
|
||||||
|
reault_header = document.getElementById("result_header");
|
||||||
|
result_header.textContent = `Results:`;
|
||||||
|
list.innerHTML = "";
|
||||||
|
results.forEach(item => {
|
||||||
|
const li = document.createElement("li");
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = item.item.url;
|
||||||
|
a.textContent = item.item.fullname;
|
||||||
|
li.appendChild(a);
|
||||||
|
list.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
9
templates/man_page.j2
Normal file
9
templates/man_page.j2
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{% extends "base.j2" %}
|
||||||
|
{% block body %}
|
||||||
|
<header class="header">
|
||||||
|
<h1>{{ header_title }}</h1>
|
||||||
|
</header>
|
||||||
|
<main class="main-content">
|
||||||
|
{{ main_content }}
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user