use anyhow::{Context, Result, bail};
use clap::{Parser, Subcommand};
use reqwest::StatusCode;
use reqwest::blocking::Client;
use reqwest::header::COOKIE;
use serde::Deserialize;
use std::fs::File;
use std::io::prelude::*;
use urlencoding::encode as encode_url;
#[derive(Parser)]
struct Args {
#[command(subcommand)]
source: InputSource,
input: Option<String>,
}
#[derive(Subcommand)]
enum InputSource {
Title {
title: String,
key: Option<String>,
},
Url {
url: String,
},
JsonUrl {
url: String,
},
Local {
path: String,
},
JsonLocal {
path: String,
},
}
#[derive(Deserialize)]
struct PageJson {
lines: Vec<LineJson>,
}
#[derive(Deserialize)]
struct LineJson {
text: String,
}
fn main() -> Result<()> {
let args = Args::parse();
let input = match args.source {
InputSource::Title { title, key } => fetch_by_page_title(&title, &key)?,
InputSource::Local { path } => read_plain_local(&path)?,
InputSource::Url { url } => fetch_plain_by_url(&url, &None)?,
InputSource::JsonLocal { path } => read_json_local(&path)?,
InputSource::JsonUrl { url } => fetch_json_by_url(&url, &None)?,
};
let content = input.as_str();
let result = cosy::parse(content, &());
match result {
Ok(nodes) => println!("{:#?}", nodes),
Err(e) => println!("Error: {}", e),
}
Ok(())
}
fn read_plain_local(path: &str) -> Result<String> {
let mut content = String::new();
let mut f = File::open(path).context("File not found.")?;
f.read_to_string(&mut content)
.context("Something went wrong reading the file.")?;
Ok(content)
}
fn fetch_plain_by_url(url: &str, key: &Option<String>) -> Result<String> {
let mut req = Client::new().get(url);
if let Some(key) = key {
req = req.header(COOKIE, format!("connect.sid={key};"))
}
let res = req.send().context("Failed to send request.")?;
match res.status() {
StatusCode::OK => (),
StatusCode::UNAUTHORIZED => {
bail!("Unauthorized to fetch content. Please provide your api key.")
}
StatusCode::NOT_FOUND => bail!("Content not found"),
_ => bail!("Failed to fetch content."),
};
let plain = res.text()?;
Ok(plain)
}
fn fetch_by_page_title(title: &str, key: &Option<String>) -> Result<String> {
let split_title: Vec<&str> = title.split('/').collect();
if split_title.len() != 2 || split_title[0].is_empty() || split_title[1].is_empty() {
bail!("Invalid page title. It must be `project/page` style.")
}
let project_name = encode_url(split_title[0]);
let page_title = encode_url(split_title[1]);
let url = format!("https://scrapbox.io/api/pages/{project_name}/{page_title}");
fetch_json_by_url(&url, key)
}
fn read_json_local(path: &str) -> Result<String> {
let json = read_plain_local(path)?;
parse_json(&json)
}
fn fetch_json_by_url(url: &str, key: &Option<String>) -> Result<String> {
let json = fetch_plain_by_url(url, key)?;
parse_json(&json)
}
fn parse_json(json: &str) -> Result<String> {
let page: PageJson = serde_json::from_str(json)?;
let mut content = String::new();
for line in page.lines {
content.push_str(&line.text);
content.push('\n');
}
Ok(content)
}