Compare commits

...

9 Commits

Author SHA1 Message Date
Stefan Menner
41b1930c70 update 2025-06-16 16:52:00 +02:00
ed64cd886b Update for db 2025-06-16 08:03:02 +02:00
711c27967b t2 2025-06-07 12:05:07 +02:00
240240ba3c t 2025-06-07 12:02:25 +02:00
Stefan Menner
c1c0a0601c Update parser 2025-06-03 17:38:11 +02:00
c24a20ad3f Parser update 2025-06-01 21:32:06 +02:00
b08cb54acd update 2025-05-29 11:43:28 +02:00
Stefan Menner
999d8b92aa Update 2025-05-28 17:16:10 +02:00
ef09a717dd Change metadata 2025-05-27 15:43:44 +02:00
26 changed files with 11918 additions and 1 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
DATABASE_URL=postgres://postgres:.admin1@localhost/no_man_sky2

5
.gitignore vendored
View File

@@ -20,3 +20,8 @@ Cargo.lock
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.env
.env.*
!.env.example
!.env.*.example
# Ignore the .env file that contains sensitive information

45
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,45 @@
{
// Verwendet IntelliSense zum Ermitteln möglicher Attribute.
// Zeigen Sie auf vorhandene Attribute, um die zugehörigen Beschreibungen anzuzeigen.
// Weitere Informationen finden Sie unter https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'no-man-sky'",
"cargo": {
"args": [
"build",
"--bin=no-man-sky",
"--package=no-man-sky"
],
"filter": {
"name": "no-man-sky",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
},
{
"type": "lldb",
"request": "launch",
"name": "Debug unit tests in executable 'no-man-sky'",
"cargo": {
"args": [
"test",
"--no-run",
"--bin=no-man-sky",
"--package=no-man-sky"
],
"filter": {
"name": "no-man-sky",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

11
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"cSpell.words": [
"Herstellung",
"Kochen",
"Quelle",
"Raffination",
"serde",
"Stück",
"Verwendung"
]
}

18
Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "no-man-sky"
version = "0.1.0"
edition = "2024"
publish = ["merlin"]
description = "Utility functions to read environment with fallback and values from a file"
license = "MIT"
repository = "ssh://git@gitea.merlinserver.de:2222/Stefan/merlin_env_helper.git"
[dependencies]
diesel = { version= "2.2.10", features = ["serde_json", "postgres", "uuid"]}
env_logger = "0.11.8"
log = "0.4.27"
merlin_env_helper = { version = "0.2.0", registry = "merlin" }
regex = "1.11.1"
reqwest = {version="0.12.15", features=["blocking"]}
#scraper = "0.23.1"
select = "0.6.1"

View File

@@ -1,3 +1,5 @@
# no-man-sky-wiki
No-man-sky resource parser & resource lib
No-man-sky resource parser & resource lib
Test1

9
diesel.toml Normal file
View File

@@ -0,0 +1,9 @@
# For documentation on how to configure this file,
# see https://diesel.rs/guides/configuring-diesel-cli
[print_schema]
file = "src/db/schema.rs"
custom_type_derives = ["diesel::query_builder::QueryId", "Clone"]
[migrations_directory]
dir = "/home/stefan/projects/rust/no-man-sky-wiki/migrations"

0
migrations/.keep Normal file
View File

View File

@@ -0,0 +1,6 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
DROP FUNCTION IF EXISTS diesel_set_updated_at();

View File

@@ -0,0 +1,36 @@
-- This file was automatically created by Diesel to setup helper functions
-- and other internal bookkeeping. This file is safe to edit, any future
-- changes will be added to existing projects as new migrations.
-- Sets up a trigger for the given table to automatically set a column called
-- `updated_at` whenever the row is modified (unless `updated_at` was included
-- in the modified columns)
--
-- # Example
--
-- ```sql
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
--
-- SELECT diesel_manage_updated_at('users');
-- ```
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
BEGIN
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
BEGIN
IF (
NEW IS DISTINCT FROM OLD AND
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
) THEN
NEW.updated_at := current_timestamp;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

View File

@@ -0,0 +1,11 @@
-- This file should undo anything in `up.sql`
DROP INDEX IF EXISTS idx_icon_name;
DROP INDEX IF EXISTS idx_resource_name;
DROP INDEX IF EXISTS idx_resource_title;
DROP INDEX IF EXISTS idx_recipe_type;
DROP TABLE IF EXISTS "ingredient";
DROP TABLE IF EXISTS "recipe";
DROP TABLE IF EXISTS "resource";
DROP TABLE IF EXISTS "icon";

View File

@@ -0,0 +1,56 @@
CREATE TABLE "icon"(
"id" UUID NOT NULL PRIMARY KEY,
"name" VARCHAR(255) NOT NULL,
"content_type" VARCHAR(255),
"url" VARCHAR(512),
"width" INT4,
"height" INT4,
"state" JSON
);
CREATE TABLE "resource"(
"id" UUID NOT NULL PRIMARY KEY,
"name" VARCHAR(255) NOT NULL,
"title" VARCHAR(255) NOT NULL,
"url" VARCHAR(512),
"icon" UUID,
"state" JSON,
FOREIGN KEY ("icon") REFERENCES "icon"("id")
);
CREATE TABLE "recipe"(
"id" UUID NOT NULL PRIMARY KEY,
"resource" UUID NOT NULL,
"recipe_type" VARCHAR(50) NOT NULL,
"duration" INT4 NOT NULL,
"state" JSON,
FOREIGN KEY ("resource") REFERENCES "resource"("id")
);
CREATE TABLE "ingredient"(
"id" UUID NOT NULL PRIMARY KEY,
"resource" UUID NOT NULL,
"quantity" INT4 NOT NULL,
"state" JSON,
"recipe" UUID NOT NULL,
FOREIGN KEY ("resource") REFERENCES "resource"("id"),
FOREIGN KEY ("recipe") REFERENCES "recipe"("id")
);
CREATE INDEX IF NOT EXISTS idx_icon_name
ON icon USING btree
(name COLLATE pg_catalog."default" ASC NULLS LAST);
CREATE INDEX IF NOT EXISTS idx_recipe_type
ON recipe USING btree
(recipe_type COLLATE pg_catalog."default" ASC NULLS LAST);
CREATE INDEX IF NOT EXISTS idx_resource_name
ON resource USING btree
(name COLLATE pg_catalog."default" ASC NULLS LAST);
CREATE INDEX IF NOT EXISTS idx_resource_title
ON resource USING btree
(title COLLATE pg_catalog."default" ASC NULLS LAST);

188
snipped.html Normal file
View File

@@ -0,0 +1,188 @@
<span
class="resource-substanzmithoherenergie"
style="border: 1px solid #d3d3d3"
>
<span class="mw-valign-text-bottom" typeof="mw:File">
<a href="/de/wiki/Diwasserstoff" title="Diwasserstoff">
<img
class="mw-file-element lazyload"
data-image-key="SUBSTANCE.LAUNCHSUB.1.png"
data-image-name="SUBSTANCE.LAUNCHSUB.1.png"
data-relevant="1"
data-src="https://static.wikia.nocookie.net/nomanssky_gamepedia/images/0/03/SUBSTANCE.LAUNCHSUB.1.png/revision/latest/scale-to-width-down/18?cb=20180725071716"
decoding="async"
height="18"
loading="lazy"
src="data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D"
width="18"
/>
</a>
</span>
</span>
<a href="/de/wiki/Diwasserstoff" title="Diwasserstoff">
<span class="itemlink ajaxttlink">Diwasserstoff</span>
</a>
x1&nbsp;&nbsp;+&nbsp;&nbsp;
<span
class="resource-konzentrierterflüssigtreibstoff"
style="border: 1px solid #d3d3d3"
>
<span class="mw-valign-text-bottom" typeof="mw:File">
<a href="/de/wiki/Sauerstoff" title="Sauerstoff">
<img
class="mw-file-element lazyload"
data-image-key="SUBSTANCE.AIR.1.png"
data-image-name="SUBSTANCE.AIR.1.png"
data-relevant="1"
data-src="https://static.wikia.nocookie.net/nomanssky_gamepedia/images/e/ec/SUBSTANCE.AIR.1.png/revision/latest/scale-to-width-down/18?cb=20221007220715"
decoding="async"
height="18"
loading="lazy"
src="data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D"
width="18"
/>
</a>
</span>
</span>
<a href="/de/wiki/Sauerstoff" title="Sauerstoff">
<span class="itemlink ajaxttlink">Sauerstoff</span>
</a>
x1&nbsp;&nbsp;&nbsp;&nbsp;
<span
class="resource-aquatischesmineral-extrakt"
style="border: 1px solid #d3d3d3"
>
<span class="mw-valign-text-bottom" typeof="mw:File">
<a href="/de/wiki/Salz" title="Salz">
<img
class="mw-file-element lazyload"
data-image-key="SUBSTANCE.WATER.1.png"
data-image-name="SUBSTANCE.WATER.1.png"
data-relevant="1"
data-src="https://static.wikia.nocookie.net/nomanssky_gamepedia/images/9/9f/SUBSTANCE.WATER.1.png/revision/latest/scale-to-width-down/18?cb=20180726042119"
decoding="async"
height="18"
loading="lazy"
src="data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D"
width="18"
/>
</a>
</span>
</span>
<strong class="mw-selflink selflink">
<span class="itemlink ajaxttlink">Salz</span>
</strong>
x1&nbsp;&nbsp;
<small>( <i>"Schnelle Formation/Verdunstung"</i>,&nbsp;0,08 sek./Stück)</small>
<ul>
<li>
<span
class="resource-verarbeiteteswassermineral"
style="border: 1px solid #d3d3d3"
><span class="mw-valign-text-bottom" typeof="mw:File"
><a href="/de/wiki/Chlor" title="Chlor"
><img
src="data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D"
decoding="async"
loading="lazy"
width="18"
height="18"
class="mw-file-element lazyload"
data-image-name="SUBSTANCE.WATER.2.png"
data-image-key="SUBSTANCE.WATER.2.png"
data-relevant="1"
data-src="https://static.wikia.nocookie.net/nomanssky_gamepedia/images/2/25/SUBSTANCE.WATER.2.png/revision/latest/scale-to-width-down/18?cb=20180726040634" /></a></span
></span>
<a href="/de/wiki/Chlor" title="Chlor"
><span class="itemlink ajaxttlink">Chlor</span></a
>
x1&#160;&#160;&#8594;&#160;&#160;
<span
class="resource-aquatischesmineral-extrakt"
style="border: 1px solid #d3d3d3"
><span class="mw-valign-text-bottom" typeof="mw:File"
><a href="/de/wiki/Salz" title="Salz"
><img
src="data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D"
decoding="async"
loading="lazy"
width="18"
height="18"
class="mw-file-element lazyload"
data-image-name="SUBSTANCE.WATER.1.png"
data-image-key="SUBSTANCE.WATER.1.png"
data-relevant="1"
data-src="https://static.wikia.nocookie.net/nomanssky_gamepedia/images/9/9f/SUBSTANCE.WATER.1.png/revision/latest/scale-to-width-down/18?cb=20180726042119" /></a></span
></span>
<strong class="mw-selflink selflink"
><span class="itemlink ajaxttlink">Salz</span></strong
>
x2&#160;&#160;<small>(<i>"Salzproduktion"</i>,&#160;0,24 sek./Stück)</small>
</li>
<li>
<span
class="resource-substanzmithoherenergie"
style="border: 1px solid #d3d3d3"
><span class="mw-valign-text-bottom" typeof="mw:File"
><a href="/de/wiki/Diwasserstoff" title="Diwasserstoff"
><img
src="data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D"
decoding="async"
loading="lazy"
width="18"
height="18"
class="mw-file-element lazyload"
data-image-name="SUBSTANCE.LAUNCHSUB.1.png"
data-image-key="SUBSTANCE.LAUNCHSUB.1.png"
data-relevant="1"
data-src="https://static.wikia.nocookie.net/nomanssky_gamepedia/images/0/03/SUBSTANCE.LAUNCHSUB.1.png/revision/latest/scale-to-width-down/18?cb=20180725071716" /></a></span
></span>
<a href="/de/wiki/Diwasserstoff" title="Diwasserstoff"
><span class="itemlink ajaxttlink">Diwasserstoff</span></a
>
x1&#160;&#160;+&#160;&#160;
<span
class="resource-konzentrierterflüssigtreibstoff"
style="border: 1px solid #d3d3d3"
><span class="mw-valign-text-bottom" typeof="mw:File"
><a href="/de/wiki/Sauerstoff" title="Sauerstoff"
><img
src="data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D"
decoding="async"
loading="lazy"
width="18"
height="18"
class="mw-file-element lazyload"
data-image-name="SUBSTANCE.AIR.1.png"
data-image-key="SUBSTANCE.AIR.1.png"
data-relevant="1"
data-src="https://static.wikia.nocookie.net/nomanssky_gamepedia/images/e/ec/SUBSTANCE.AIR.1.png/revision/latest/scale-to-width-down/18?cb=20221007220715" /></a></span
></span>
<a href="/de/wiki/Sauerstoff" title="Sauerstoff"
><span class="itemlink ajaxttlink">Sauerstoff</span></a
>
x1&#160;&#160;&#8594;&#160;&#160;<span
class="resource-aquatischesmineral-extrakt"
style="border: 1px solid #d3d3d3"
><span class="mw-valign-text-bottom" typeof="mw:File"
><a href="/de/wiki/Salz" title="Salz"
><img
src="data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D"
decoding="async"
loading="lazy"
width="18"
height="18"
class="mw-file-element lazyload"
data-image-name="SUBSTANCE.WATER.1.png"
data-image-key="SUBSTANCE.WATER.1.png"
data-relevant="1"
data-src="https://static.wikia.nocookie.net/nomanssky_gamepedia/images/9/9f/SUBSTANCE.WATER.1.png/revision/latest/scale-to-width-down/18?cb=20180726042119" /></a></span
></span>
<strong class="mw-selflink selflink"
><span class="itemlink ajaxttlink">Salz</span></strong
>
x1&#160;&#160;<small
>(<i>"Schnelle Formation/Verdunstung"</i>,&#160;0,08 sek./Stück)</small
>
</li>
</ul>

2
src/db/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod models;
pub use models::*;

47
src/db/models.rs Normal file
View File

@@ -0,0 +1,47 @@
// Generated by diesel_ext
#![allow(unused)]
#![allow(clippy::all)]
use diesel::{
prelude::Queryable,
sql_types::{Json, Uuid},
};
#[derive(Queryable, Debug)]
pub struct RowIcon {
pub id: Uuid,
pub name: String,
pub content_type: Option<String>,
pub url: Option<String>,
pub width: Option<i32>,
pub height: Option<i32>,
pub state: Option<Json>,
}
#[derive(Queryable, Debug)]
pub struct RowIngredient {
pub id: Uuid,
pub resource: Uuid,
pub quantity: i32,
pub state: Option<Json>,
pub recipe: Uuid,
}
#[derive(Queryable, Debug)]
pub struct RowRecipe {
pub id: Uuid,
pub resource: Uuid,
pub recipe_type: String,
pub duration: i32,
pub state: Option<Json>,
}
#[derive(Queryable, Debug)]
pub struct RowResource {
pub id: Uuid,
pub name: String,
pub title: String,
pub url: Option<String>,
pub icon: Option<Uuid>,
pub state: Option<Json>,
}

63
src/db/schema.rs Normal file
View File

@@ -0,0 +1,63 @@
// @generated automatically by Diesel CLI.
diesel::table! {
icon (id) {
id -> Uuid,
#[max_length = 255]
name -> Varchar,
#[max_length = 255]
content_type -> Nullable<Varchar>,
#[max_length = 512]
url -> Nullable<Varchar>,
width -> Nullable<Int4>,
height -> Nullable<Int4>,
state -> Nullable<Json>,
}
}
diesel::table! {
ingredient (id) {
id -> Uuid,
resource -> Uuid,
quantity -> Int4,
state -> Nullable<Json>,
recipe -> Uuid,
}
}
diesel::table! {
recipe (id) {
id -> Uuid,
resource -> Uuid,
#[max_length = 50]
recipe_type -> Varchar,
duration -> Int4,
state -> Nullable<Json>,
}
}
diesel::table! {
resource (id) {
id -> Uuid,
#[max_length = 255]
name -> Varchar,
#[max_length = 255]
title -> Varchar,
#[max_length = 512]
url -> Nullable<Varchar>,
icon -> Nullable<Uuid>,
state -> Nullable<Json>,
}
}
diesel::joinable!(ingredient -> recipe (recipe));
diesel::joinable!(ingredient -> resource (resource));
diesel::joinable!(recipe -> resource (resource));
diesel::joinable!(resource -> icon (icon));
diesel::allow_tables_to_appear_in_same_query!(
icon,
ingredient,
recipe,
resource,
);

35
src/main.rs Normal file
View File

@@ -0,0 +1,35 @@
mod db;
mod parse;
mod types;
use std::{fs::File, io::Read};
use parse::parse;
fn main() -> Result<(), Box<dyn std::error::Error>> {
env_logger::init();
let html = read("test_mordit.html")?;
parse(&html);
Ok(())
}
fn _download_file(url: &str, _path: &str) -> Result<String, Box<dyn std::error::Error>> {
// Some simple CLI args requirements...
eprintln!("Fetching {url:?}...");
// reqwest::blocking::get() is a convenience function.
//
// In most cases, you should create/build a reqwest::Client and reuse
// it for all requests.
let res = reqwest::blocking::get(url)?;
let body = res.text()?;
Ok(body)
}
fn read(path: &str) -> Result<String, std::io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}

581
src/parse/mod.rs Normal file
View File

@@ -0,0 +1,581 @@
use crate::types::{Duration, Icon, Ingredient, Recipe, RecipeType, Resource, ResourceState};
use regex::Regex;
use select::{
document::Document,
node::Node,
predicate::{Attr, Name, Or},
};
use std::collections::HashMap;
#[derive(Debug, PartialEq, Eq)]
enum ParseType {
Link { url: String, title: String },
Img(Icon),
Count(u32),
Resource(String),
ResourceAdd,
ResourceLast,
DashDash,
Duration(Duration),
}
type ResourceTmp = (
Option<String>, // name
Option<String>, // title
Option<Icon>, // icon
Option<u32>, // count
Option<Duration>, // duration
);
pub fn parse(html: &str) {
let document = Document::from(html);
let mut map_resource: HashMap<String, Resource> = HashMap::new();
let mut recipes: Vec<Recipe> = Vec::new();
parse_source(&document, &mut map_resource, &mut recipes);
parse_dst(
&document,
"Raffination",
RecipeType::Refining,
&mut map_resource,
&mut recipes,
);
parse_dst(
&document,
"Herstellung",
RecipeType::Production,
&mut map_resource,
&mut recipes,
);
parse_dst(
&document,
"Raffination_2",
RecipeType::Refining,
&mut map_resource,
&mut recipes,
);
parse_dst(
&document,
"Kochen",
RecipeType::Cooking,
&mut map_resource,
&mut recipes,
);
print_recipe(&recipes, RecipeType::Refining);
print_recipe(&recipes, RecipeType::Production);
print_recipe(&recipes, RecipeType::Cooking);
}
fn first<T>(iter: &mut impl Iterator<Item = T>) -> Option<T> {
iter.next()
}
fn first_child_element(node: Node<'_>) -> Option<Node<'_>> {
if node.children().next().is_none() {
return None;
}
let mut next = node.children().next();
while next.is_some() {
let child = next.unwrap();
if child.name().is_some() {
return Some(child);
}
next = child.next();
}
None
}
fn parse_resource_items(
resource_items: Vec<ParseType>,
recipe_type: RecipeType,
map_resource: &mut HashMap<String, Resource>,
) -> Option<Recipe> {
let mut tmp_resource: ResourceTmp = (None, None, None, None, None);
let mut ingredient_to_add: Vec<Ingredient> = Vec::new();
let mut not_add = false;
for item in resource_items.iter() {
match item {
ParseType::Link { url, title } => {
if tmp_resource.0.is_none() {
tmp_resource.0 = Some(title.to_string());
}
if tmp_resource.1.is_none() {
tmp_resource.1 = Some(url.to_string());
}
// println!("Link: {} - {}", url, title);
}
ParseType::Img(icon) => {
if tmp_resource.2.is_none() {
tmp_resource.2 = Some(icon.clone());
}
}
ParseType::Count(count) => {
if tmp_resource.3.is_none() {
tmp_resource.3 = Some(*count);
}
}
ParseType::Resource(resource) => {
// println!("Resource: {}", resource);
if tmp_resource.0.is_none() {
tmp_resource.0 = Some(resource.to_string());
}
}
ParseType::ResourceAdd => {
if !not_add {
add(&tmp_resource, map_resource, &mut ingredient_to_add);
}
not_add = false;
tmp_resource = (None, None, None, None, None); // Reset for next resource
// println!("ResourceAdd");
}
ParseType::ResourceLast => {
if !not_add {
add(&tmp_resource, map_resource, &mut ingredient_to_add);
}
not_add = false;
tmp_resource = (None, None, None, None, None); // Reset for next resource
// println!("ResourceLast");
}
ParseType::Duration(duration) => {
// println!(">>> Duration: {} {}", duration, unit);
if tmp_resource.4.is_none() {
tmp_resource.4 = Some(duration.clone());
}
}
ParseType::DashDash => {
not_add = false;
tmp_resource = (None, None, None, None, None); // Reset for next resource
}
}
}
let (_, ingredient) = create_resource_and_ingredient(&tmp_resource, map_resource);
if !ingredient_to_add.is_empty() {
let recipe = crate::types::Recipe {
recipe_type: recipe_type,
resource: ingredient,
duration: tmp_resource.4.unwrap_or(Duration {
millis: 0,
unit: "Stück".to_string(),
}),
ingredients: ingredient_to_add,
};
return Some(recipe);
}
None
}
fn print_recipe(recipes: &Vec<Recipe>, recipe_type: RecipeType) {
for recipe in recipes
.iter()
.filter(|recipe| recipe.recipe_type == recipe_type)
{
println!("Recipe Type: {:?}", recipe.recipe_type);
println!(
"Resource: {} ({})",
recipe.resource.resource.name, recipe.resource.quantity
);
println!("Duration: {} ms", recipe.duration.millis);
println!("Ingredients:");
for ingredient in &recipe.ingredients {
println!(
"- {} ({} x {})",
ingredient.resource.name, ingredient.quantity, ingredient.resource.title
);
}
println!();
}
}
fn parse_dst(
document: &Document,
id: &str,
recipe_type: RecipeType,
map_resource: &mut HashMap<String, Resource>,
recipes: &mut Vec<Recipe>,
) -> bool {
let mut dest = document.find(Attr("id", id));
let dest = first(&mut dest);
if dest.is_none() {
eprintln!("No element found with the selector '#{}'", id);
return false;
}
let dst = dest.unwrap();
let mut dst = dst.parent().unwrap();
let mut elt_ul = None;
while dst.next().is_some() {
dst = dst.next().unwrap();
if let Some(name) = dst.name() {
if name == "ul" {
elt_ul = Some(dst);
break;
}
if name == "h2" {
break;
}
let first_child = first_child_element(dst);
if name == "h3"
&& first_child.is_some()
&& first_child.unwrap().name().unwrap() == "span"
&& first_child.unwrap().attr("id").is_some()
{
break;
}
}
}
if elt_ul.is_none() {
return false;
}
let elt_ul = elt_ul.unwrap();
let li = elt_ul.find(Name("li"));
for item in li {
if let Some(recipe) = parse_li_to_resource(&item, recipe_type.clone(), map_resource) {
recipes.push(recipe);
}
}
return true;
}
fn parse_source(
document: &Document,
map_resource: &mut HashMap<String, Resource>,
recipes: &mut Vec<Recipe>,
) -> bool {
let mut source = document.find(Attr("id", "Quelle"));
let source = first(&mut source);
if source.is_none() {
return false;
}
let source = source.unwrap();
let mut source = source.parent().unwrap();
let mut c = 0;
let mut elt_ul = None;
while source.next().is_some() {
source = source.next().unwrap();
if let Some(name) = source.name() {
if name == "ul" {
c += 1;
if c > 1 {
elt_ul = Some(source);
break;
}
}
if name == "h2" {
break;
}
}
}
if elt_ul.is_none() {
return false;
}
let elt_ul = elt_ul.unwrap();
let li = elt_ul.find(Name("li"));
for item in li {
if let Some(recipe) = parse_li_to_resource(&item, RecipeType::Refining, map_resource) {
recipes.push(recipe);
}
}
return true;
}
fn parse_li_to_resource(
item: &Node<'_>,
recipe_type: RecipeType,
map_resource: &mut HashMap<String, Resource>,
) -> Option<Recipe> {
if item.children().next().is_none() {
return None;
}
let mut resource_items: Vec<ParseType> = Vec::new();
let selector = item.find(Or(
Name("strong"),
Or(Or(Name("span"), Name("a")), Or(Name("img"), Name("small"))),
));
for child in selector {
let name = child.name().unwrap();
if name == "a" && child.attr("href").is_some() && child.attr("title").is_some() {
let txt = get_text_next(&child);
resource_items.push(ParseType::Link {
url: child.attr("href").unwrap().to_string(),
title: child.attr("title").unwrap().to_string(),
});
if !txt.is_empty() {
parse_text(&txt, &mut resource_items);
}
if txt == "--" {
resource_items.push(ParseType::DashDash);
}
} else if name == "img"
&& child.attr("data-src").is_some()
&& child.attr("width").is_some()
&& child.attr("height").is_some()
&& child.attr("data-image-name").is_some()
{
let url = child.attr("data-src").unwrap();
let name = child.attr("data-image-name").unwrap();
let width = child.attr("width").unwrap().parse().unwrap_or(0);
let height = child.attr("height").unwrap().parse().unwrap_or(0);
resource_items.push(ParseType::Img(Icon {
name: name.to_string(),
url: url.to_string(),
width,
height,
content_type: "image/png".to_string(), // Assuming PNG, adjust as needed
}));
} else if name == "span"
&& !child.text().is_empty()
&& (child.parent().unwrap().name().unwrap() == "strong"
|| child.parent().unwrap().name().unwrap() == "span")
{
let txt = child.text().trim().to_string();
resource_items.push(ParseType::Resource(txt));
let txt = get_text_next(&child.parent().unwrap());
parse_text(&txt, &mut resource_items);
} else if name == "strong" {
// let txt = get_text_next(&child);
// parse_text(&txt, &mut resource_items);
} else if name == "small" {
let txt = get_text(&child);
parse_text(&txt, &mut resource_items);
}
}
parse_resource_items(resource_items, recipe_type, map_resource)
}
fn to_name(name: &str) -> String {
normalize_text(name).replace(" ", "_").to_lowercase()
}
fn create_resource_and_ingredient(
tmp_resource: &ResourceTmp,
map_resource: &mut HashMap<String, Resource>,
) -> (Resource, Ingredient) {
let title = tmp_resource.0.as_ref().unwrap().clone();
let name = to_name(&title);
let url = tmp_resource.1.clone();
let icon = tmp_resource.2.clone();
let count = tmp_resource.3.unwrap_or(1);
let mut resource = Resource {
name: name.clone(),
title,
url,
icon,
state: ResourceState::Unparsed,
};
if map_resource.contains_key(&name) {
let res = map_resource.get_mut(&name).unwrap();
if res.url.is_some() {
resource.url = res.url.clone();
}
if res.icon.is_some() {
resource.icon = res.icon.clone();
}
}
let ingredient = Ingredient {
resource: resource.clone(),
quantity: count,
};
(resource, ingredient)
}
fn add(
tmp_resource: &ResourceTmp,
map_resource: &mut HashMap<String, Resource>,
ingredient_to_add: &mut Vec<Ingredient>,
) {
if tmp_resource.0.is_none() {
return;
}
let (resource, ingredient) = create_resource_and_ingredient(tmp_resource, map_resource);
map_resource.insert(resource.name.clone(), resource);
ingredient_to_add.push(ingredient);
}
fn normalize_text(text: &str) -> String {
let mut text = text
.trim()
.replace('\n', " ")
.replace('\r', " ")
.replace('\t', " ");
for c in text.clone().chars() {
if c.is_control() || c.is_whitespace() {
text = text.replace(c, " ");
}
}
while text.contains(" ") {
text = text.replace(" ", " ");
}
text
}
fn get_text(node: &Node<'_>) -> String {
let mut text = String::new();
if node.as_text().is_some() {
text.push_str(node.as_text().unwrap().to_string().as_str());
}
if node.children().next().is_some() {
for child in node.descendants() {
if child.as_text().is_some() {
text.push_str(child.as_text().unwrap().to_string().as_str());
}
}
}
if node.next().is_some() {
let mut next = node.next();
while next.is_some() {
let next_node = next.unwrap();
if next_node.as_text().is_some() {
text.push_str(next_node.as_text().unwrap());
} else {
break;
}
next = next_node.next();
}
}
return normalize_text(&text);
}
fn get_text_next(node: &Node<'_>) -> String {
if node.as_text().is_some() {
return normalize_text(&node.as_text().unwrap());
}
let next = node.next();
if !next.is_some() {
return String::new();
}
let next = next.unwrap();
if next.as_text().is_some() {
let mut text = next.as_text().unwrap().trim().to_string();
let mut next = next.next();
while next.is_some() {
let node = next.unwrap();
next = node.next();
if node.as_text().is_some() {
text.push_str(node.as_text().unwrap().trim());
} else if node.name().is_some() && node.name().unwrap() == "i" {
text.push_str(node.text().trim());
} else {
break;
}
}
return normalize_text(&text);
}
String::new()
}
fn parse_text(text: &str, resource_items: &mut Vec<ParseType>) {
let reg_count_next = Regex::new(r"^\s*x(?<count>\d+)\s+(?<end>[→+])\s*$").unwrap();
let reg_count = Regex::new(r"^\s*x(?<count>\d+)\s*.*$").unwrap();
let reg_duration =
Regex::new(r"^.*\(.*(?<duration>\d+(|,\d+))\ssek\./(?<unit>\w+)\s*\)$").unwrap();
if let Some(res) = reg_count_next.captures(text) {
let count = res.name("count").unwrap().as_str().parse().unwrap_or(0);
resource_items.push(ParseType::Count(count));
let end = res.name("end").unwrap().as_str().to_string();
if end == "+" {
resource_items.push(ParseType::ResourceAdd);
} else {
resource_items.push(ParseType::ResourceLast);
}
return;
}
if let Some(res) = reg_count.captures(text) {
let count = res.name("count").unwrap().as_str().parse().unwrap_or(0);
resource_items.push(ParseType::Count(count));
}
if let Some(res) = reg_duration.captures(text) {
let duration_str = res.name("duration").unwrap().as_str();
let duration: f64 = duration_str.replace(',', ".").parse().unwrap_or(0.0);
let unit = res.name("unit").unwrap().as_str().to_string();
let duration: u64 = (duration * 1000.0) as u64; // Convert to milliseconds
resource_items.push(ParseType::Duration(Duration {
millis: duration,
unit,
}));
}
}

8
src/test.html Normal file
View File

@@ -0,0 +1,8 @@
<html>
<body>
<img
src="data:image/gif;base64,R0lGODlhAQABAIABAAAAAP///yH5BAEAAAEALAAAAAABAAEAQAICTAEAOw%3D%3D"
width="18"
/>
</body>
</html>

2
src/types/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod types;
pub use types::*;

61
src/types/types.rs Normal file
View File

@@ -0,0 +1,61 @@
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum ResourceState {
Parsed(bool),
Unparsed,
}
#[derive(Debug, PartialEq, Eq)]
pub struct Icon {
pub name: String,
pub url: String,
pub width: u32,
pub height: u32,
pub content_type: String,
}
impl Clone for Icon {
fn clone(&self) -> Self {
Icon {
name: self.name.clone(),
url: self.url.clone(),
width: self.width,
height: self.height,
content_type: self.content_type.clone(),
}
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Resource {
pub name: String,
pub title: String,
pub url: Option<String>,
pub icon: Option<Icon>,
pub state: ResourceState,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Ingredient {
pub resource: Resource,
pub quantity: u32,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum RecipeType {
Production,
Refining,
Cooking,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Duration {
pub millis: u64,
pub unit: String,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Recipe {
pub recipe_type: RecipeType,
pub resource: Ingredient,
pub duration: Duration,
pub ingredients: Vec<Ingredient>,
}

3
src/util/cmd.rs Normal file
View File

@@ -0,0 +1,3 @@
pub fn parse_command_line_args() -> Vec<String> {
std::env::args().skip(1).collect()
}

2
src/util/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod cmd;
pub use cmd::*;

3347
test.html Normal file

File diff suppressed because one or more lines are too long

3501
test_mordit.html Normal file

File diff suppressed because one or more lines are too long

3877
test_vc.html Normal file

File diff suppressed because one or more lines are too long