Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ jobs:
run: |
python extensions/cli/ccfddl/convert_to_ical.py
mv *.ics public/conference/
- name: Generate RSS feeds
run: |
python extensions/cli/ccfddl/convert_to_rss.py
mv *.xml public/conference/
# Install Rust Nightly Toolchain, with Clippy & Rustfmt
- name: Install nightly Rust
uses: dtolnay/rust-toolchain@nightly
Expand Down
176 changes: 176 additions & 0 deletions extensions/cli/ccfddl/convert_to_rss.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import xml.etree.ElementTree as ET
from datetime import datetime, timedelta, timezone
from email.utils import format_datetime

from convert_to_ical import load_mapping, get_timezone, reverse_index

import yaml


def convert_to_rss(
file_paths: list[str], output_path: str, lang: str = "en", SUB_MAPPING={}
):
rss = ET.Element("rss", version="2.0")
channel = ET.SubElement(rss, "channel")

ET.SubElement(channel, "title").text = (
"CCF Conference Deadlines" if lang == "en" else "CCF 会议截止日期"
)
ET.SubElement(channel, "link").text = "https://ccfddl.com"
ET.SubElement(channel, "description").text = (
"Conference submission deadline tracking"
if lang == "en"
else "会议投稿截止日期追踪"
)
ET.SubElement(channel, "language").text = "en" if lang == "en" else "zh-CN"
ET.SubElement(channel, "lastBuildDate").text = format_datetime(
datetime.now(timezone.utc)
)

for file_path in file_paths:
with open(file_path, "r", encoding="utf-8") as f:
conferences = yaml.safe_load(f)

for conf_data in conferences:
title = conf_data["title"]
sub = conf_data["sub"]
sub_chinese = SUB_MAPPING.get(sub, sub)
rank = conf_data["rank"]
dblp = conf_data["dblp"]

for conf in conf_data["confs"]:
year = conf["year"]
link = conf["link"]
timeline = conf["timeline"]
timezone_str = conf["timezone"]
place = conf["place"]
date = conf["date"]

for entry in timeline:
try:
tz = get_timezone(timezone_str)
except ValueError:
continue

deadlines_to_process = []

if "abstract_deadline" in entry:
deadlines_to_process.append(
(
("摘要截稿", "Abstract Deadline"),
entry["abstract_deadline"],
"abstract",
)
)

if "deadline" in entry:
deadlines_to_process.append(
(("截稿日期", "Deadline"), entry["deadline"], "deadline")
)

if not deadlines_to_process:
continue

for deadline_type, deadline_str, type_key in deadlines_to_process:
if deadline_str == "TBD":
continue

try:
deadline_dt = datetime.strptime(
deadline_str, "%Y-%m-%d %H:%M:%S"
)
except ValueError:
try:
deadline_dt = datetime.strptime(
deadline_str, "%Y-%m-%d"
)
except ValueError:
continue

aware_dt = deadline_dt.replace(tzinfo=tz)

item = ET.SubElement(channel, "item")

if lang == "en":
summary = f"{title} {year} {deadline_type[1]}"
else:
summary = f"{title} {year} {deadline_type[0]}"

if "comment" in entry:
summary += f" [{entry['comment']}]"

ET.SubElement(item, "title").text = summary
ET.SubElement(item, "link").text = link

# Build description
level_desc = [
f"CCF {rank['ccf']}" if rank["ccf"] != "N" else None,
f"CORE {rank['core']}"
if rank.get("core", "N") != "N"
else None,
f"THCPL {rank['thcpl']}"
if rank.get("thcpl", "N") != "N"
else None,
]
level_desc = [x for x in level_desc if x]
level_str = ", ".join(level_desc) if level_desc else None

if lang == "en":
desc_lines = [
conf_data["description"],
f"Date: {date}",
f"Location: {place}",
f"Deadline ({timezone_str}): {deadline_str}",
f"Category: {sub_chinese} ({sub})",
level_str,
f"Conference Website: {link}",
f"DBLP: https://dblp.org/db/conf/{dblp}",
]
else:
desc_lines = [
conf_data["description"],
f"会议时间: {date}",
f"会议地点: {place}",
f"截止时间 ({timezone_str}): {deadline_str}",
f"分类: {sub_chinese} ({sub})",
level_str,
f"会议官网: {link}",
f"DBLP索引: https://dblp.org/db/conf/{dblp}",
]
desc_lines = [x for x in desc_lines if x]
ET.SubElement(item, "description").text = "\n".join(desc_lines)

ET.SubElement(item, "pubDate").text = format_datetime(aware_dt)

guid = ET.SubElement(
item, "guid", isPermaLink="false"
)
guid.text = f"{title}-{year}-{type_key}-{deadline_str}@ccfddl.com"

ET.SubElement(item, "category").text = sub

tree = ET.ElementTree(rss)
ET.indent(tree, space=" ")
with open(output_path, "wb") as f:
tree.write(f, encoding="utf-8", xml_declaration=True)


if __name__ == "__main__":
from xlin import ls, element_mapping

SUB_MAPPING = load_mapping("conference/types.yml")
paths = ls("conference", filter=lambda f: f.name != "types.yml")
index = reverse_index(paths, list(SUB_MAPPING.keys()))
for lang in ["zh", "en"]:
convert_to_rss(paths, f"deadlines_{lang}.xml", lang, SUB_MAPPING)
f = lambda key: (
len(index[key]) > 0,
convert_to_rss(
index[key],
f"deadlines_{lang}_{key.replace('*', 'star')}.xml",
lang,
SUB_MAPPING,
),
)
element_mapping(index.keys(), f, thread_pool_size=8)
print("RSS feed generation complete")
2 changes: 2 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
<!-- include support for `wasm-bindgen --weak-refs` - see: https://rustwasm.github.io/docs/wasm-bindgen/reference/weak-references.html -->
<link data-trunk rel="rust" data-wasm-opt="z" data-weak-refs />

<link rel="alternate" type="application/rss+xml" title="CCF Conference Deadlines (RSS)" href="https://ccfddl.com/conference/deadlines_en.xml" />

<link data-trunk rel="copy-dir" href="public/conference">
</head>

Expand Down
153 changes: 152 additions & 1 deletion src/components/subscription_modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ use web_sys::js_sys;
use web_sys::window;

#[derive(Clone, Debug, PartialEq)]
pub struct IcsSubscription {
pub struct SubscriptionLink {
pub url: String,
pub description: String,
}

pub type IcsSubscription = SubscriptionLink;

pub fn generate_ics_urls(
lang: &str,
subs: &HashSet<String>,
Expand Down Expand Up @@ -67,6 +69,62 @@ pub fn generate_ics_urls(
urls
}

pub fn generate_rss_urls(
lang: &str,
subs: &HashSet<String>,
ranks: &HashSet<String>,
) -> Vec<SubscriptionLink> {
let base_url = "https://ccfddl.com/conference";
let mut urls = Vec::new();

if subs.is_empty() && ranks.is_empty() {
urls.push(SubscriptionLink {
url: format!("{}/deadlines_{}.xml", base_url, lang),
description: if lang == "zh" {
"所有会议".to_string()
} else {
"All Conferences".to_string()
},
});
return urls;
}

if !subs.is_empty() && !ranks.is_empty() {
for sub in subs.iter() {
for rank in ranks.iter() {
let rank_prefix = get_rank_prefix(rank);
urls.push(SubscriptionLink {
url: format!(
"{}/deadlines_{}_{}_{}_{}.xml",
base_url, lang, rank_prefix, rank, sub
),
description: format!("{} {} {}", sub, rank_prefix.to_uppercase(), rank),
});
}
}
} else if !subs.is_empty() {
for sub in subs.iter() {
urls.push(SubscriptionLink {
url: format!("{}/deadlines_{}_{}.xml", base_url, lang, sub),
description: sub.clone(),
});
}
} else if !ranks.is_empty() {
for rank in ranks.iter() {
let rank_prefix = get_rank_prefix(rank);
urls.push(SubscriptionLink {
url: format!(
"{}/deadlines_{}_{}_{}.xml",
base_url, lang, rank_prefix, rank
),
description: format!("{} {}", rank_prefix.to_uppercase(), rank),
});
}
}

urls
}

fn get_rank_prefix(rank: &str) -> &'static str {
match rank {
"A" | "B" | "C" | "N" => "ccf",
Expand Down Expand Up @@ -173,6 +231,13 @@ pub fn SubscriptionModal(
generate_ics_urls(lang, &subs, &ranks)
});

let rss_subscriptions = Memo::new(move |_| {
let lang = if use_english.get() { "en" } else { "zh" };
let subs = check_list.get();
let ranks = rank_list.get();
generate_rss_urls(lang, &subs, &ranks)
});

let platform_hint = get_platform_instruction(use_english.get_untracked());

view! {
Expand Down Expand Up @@ -284,6 +349,92 @@ pub fn SubscriptionModal(
}}
</div>

<div style="margin-bottom: 16px;">
<div style="font-weight: 500; margin-bottom: 8px; font-size: 14px;">
{move || {
if use_english.get() {
"RSS Feed:"
} else {
"RSS 订阅:"
}
}}
</div>

{move || {
let subs = rss_subscriptions.get();
if subs.len() > 10 {
let msg = if use_english.get() {
format!(
"Too many filter combinations ({} links). Consider reducing filters or subscribe to all.",
subs.len(),
)
} else {
format!(
"筛选组合过多({} 个链接)。建议减少筛选条件或订阅全部。",
subs.len(),
)
};
view! {
<div style="color: #e6a23c; padding: 8px; background: #fdf6ec; border-radius: 4px; font-size: 13px;">
{msg}
</div>
}
.into_any()
} else {
view! {
<div>
{subs
.iter()
.enumerate()
.map(|(idx, sub)| {
let url = sub.url.clone();
let url_for_copy = url.clone();
let desc = sub.description.clone();
let label = format!("{}. {}", idx + 1, desc);
view! {
<div style="margin-bottom: 12px; padding: 10px; border: 1px solid #dcdfe6; border-radius: 6px; background: white;">
<div style="font-size: 13px; color: #333; margin-bottom: 6px; font-weight: 500;">
{label}
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<input
type="text"
readonly
value=url
style="flex: 1; padding: 6px 8px; border: 1px solid #dcdfe6; border-radius: 4px; font-size: 12px; font-family: monospace; background: #f5f7fa; outline: none;"
/>
<Button
size=ButtonSize::Small
on_click=move |_| {
copy_text_to_clipboard(&url_for_copy);
}
>
{move || {
if use_english.get() { "Copy" } else { "复制" }
}}
</Button>
</div>
</div>
}
})
.collect::<Vec<_>>()}
</div>
}
.into_any()
}
}}

<div style="font-size: 12px; color: #909399; margin-top: 4px;">
{move || {
if use_english.get() {
"Paste into your RSS reader"
} else {
"粘贴到 RSS 阅读器中"
}
}}
</div>
</div>

<div style="padding: 12px; background: #ecf5ff; border-radius: 8px; border-left: 4px solid #409eff;">
<div style="font-weight: 500; margin-bottom: 8px; font-size: 14px; color: #409eff;">
{move || {
Expand Down
Loading