diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..89cddff2b4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,26 @@ +# Git attributes for consistent line endings +# This ensures shell scripts always use LF (Unix) line endings + +# Auto detect text files and perform LF normalization +* text=auto + +# Shell scripts should always use LF line endings (even on Windows) +*.sh text eol=lf + +# Explicitly declare files that should always have specific line endings +*.yml text eol=lf +*.yaml text eol=lf +*.json text eol=lf +*.md text eol=lf +*.py text eol=lf + +# Denote binary files +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.woff binary +*.woff2 binary +*.ttf binary +*.eot binary diff --git a/.github/duplicate-detector-config.yml b/.github/duplicate-detector-config.yml new file mode 100644 index 0000000000..f60f58e556 --- /dev/null +++ b/.github/duplicate-detector-config.yml @@ -0,0 +1,44 @@ +# Duplicate Detection Configuration +# This file configures the duplicate issue and PR detection behavior + +# Similarity threshold (0.0 to 1.0) - Issues with similarity above this will be flagged +# Default: 0.75 means 75% similarity +similarity_threshold: 0.75 + +# High similarity threshold for exact matches +# Issues above this threshold will be marked as exact duplicates +# Default: 0.90 means 90% similarity +high_similarity_threshold: 0.90 + +# Maximum number of past issues/PRs to check against +# Higher numbers = more thorough but slower +# Default: 200 +max_issues_to_check: 200 + +# Automatically close issues that are exact matches (high similarity) +# Set to true to enable auto-closing +# Default: false (recommended to keep false for review) +auto_close_exact_match: false + +# Label to add for possible duplicates +label_possible_duplicate: "possible-duplicate" + +# Label to add for exact duplicates +label_exact_duplicate: "duplicate" + +# Labels to exclude from duplicate checking +# Issues with these labels won't be considered as potential duplicates +exclude_labels: + - "wontfix" + - "invalid" + - "spam" + +# Minimum text length (in characters) required for comparison +# Issues with less text will be skipped +# Default: 20 +min_text_length: 20 + +# Additional settings (optional) +# Number of top similar issues to show in the comment +# Default: 5 +max_similar_to_show: 5 diff --git a/.github/scripts/detect-duplicates.py b/.github/scripts/detect-duplicates.py new file mode 100644 index 0000000000..0fc53c4716 --- /dev/null +++ b/.github/scripts/detect-duplicates.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +""" +Duplicate Issue and Pull Request Detection Script +Detects potential duplicate issues and PRs using text similarity analysis. +""" + +import os +import sys +import argparse +from typing import List, Tuple +from github import Github, GithubException +from sklearn.feature_extraction.text import TfidfVectorizer +from sklearn.metrics.pairwise import cosine_similarity +import yaml + +# Configuration defaults +DEFAULT_SIMILARITY_THRESHOLD = 0.75 +DEFAULT_HIGH_SIMILARITY_THRESHOLD = 0.90 +DEFAULT_MAX_ISSUES_TO_CHECK = 200 +DEFAULT_AUTO_CLOSE_EXACT_MATCH = False +DEFAULT_LABEL_POSSIBLE_DUPLICATE = "possible-duplicate" +DEFAULT_LABEL_EXACT_DUPLICATE = "duplicate" +DEFAULT_MAX_SIMILAR_TO_SHOW = 5 + + +class DuplicateDetector: + """Detects duplicate issues and pull requests.""" + + def __init__(self, token: str, repo_name: str, config_path: str = None): + try: + self.github = Github(token) + self.repo = self.github.get_repo(repo_name) + self.config = self.load_config(config_path) + except Exception as e: + print(f"Error initializing GitHub connection: {e}") + sys.exit(1) + + def load_config(self, config_path: str = None) -> dict: + """Load configuration from YAML file or use defaults.""" + default_config = { + 'similarity_threshold': DEFAULT_SIMILARITY_THRESHOLD, + 'high_similarity_threshold': DEFAULT_HIGH_SIMILARITY_THRESHOLD, + 'max_issues_to_check': DEFAULT_MAX_ISSUES_TO_CHECK, + 'auto_close_exact_match': DEFAULT_AUTO_CLOSE_EXACT_MATCH, + 'label_possible_duplicate': DEFAULT_LABEL_POSSIBLE_DUPLICATE, + 'label_exact_duplicate': DEFAULT_LABEL_EXACT_DUPLICATE, + 'exclude_labels': ['wontfix', 'invalid'], + 'min_text_length': 20, + 'max_similar_to_show': DEFAULT_MAX_SIMILAR_TO_SHOW, + } + + if config_path and os.path.exists(config_path): + try: + with open(config_path, 'r') as f: + user_config = yaml.safe_load(f) + if user_config is None: + user_config = {} + elif not isinstance(user_config, dict): + raise ValueError("Config file must contain a mapping at the top level") + default_config.update(user_config) + except Exception as e: + print(f"Warning: Could not load config file: {e}") + + return default_config + + def preprocess_text(self, text: str) -> str: + """Preprocess text for comparison.""" + if not text: + return "" + # Convert to lowercase and strip whitespace + text = text.lower().strip() + # Remove URLs + import re + text = re.sub(r'http\S+|www.\S+', '', text) + # Remove markdown code blocks + text = re.sub(r'```[\s\S]*?```', '', text) + # Remove special characters but keep spaces + text = re.sub(r'[^a-z0-9\s]', ' ', text) + # Remove extra whitespace + text = ' '.join(text.split()) + return text + + def find_similar_issues(self, current_number: int, current_title: str, + current_body: str, item_type: str = 'issue') -> List[Tuple[int, str, float]]: + """Find similar issues or PRs.""" + current_text = self.preprocess_text(f"{current_title} {current_body}") + + if len(current_text) < self.config['min_text_length']: + print(f"Text too short for meaningful comparison: {len(current_text)} chars") + return [] + + # Get existing items to compare against + if item_type == 'issue': + items = self.repo.get_issues(state='all') + else: + items = self.repo.get_pulls(state='all') + + # First pass: collect all candidate items and their texts + candidates = [] # List of (item_number, item_title, item_text) + checked_count = 0 + + try: + for item in items: + if checked_count >= self.config['max_issues_to_check']: + break + + # Skip the current item + if item.number == current_number: + continue + + # Skip pull requests when checking issues (PRs are returned by get_issues API) + if item_type == 'issue' and hasattr(item, 'pull_request') and item.pull_request: + continue + + try: + # Skip items with excluded labels + item_labels = [label.name for label in item.labels] + if any(label in self.config['exclude_labels'] for label in item_labels): + continue + + # Preprocess and store candidate text + item_text = self.preprocess_text(f"{item.title} {item.body or ''}") + if item_text: # Only include non-empty texts + candidates.append((item.number, item.title, item_text)) + except Exception as e: + print(f"Warning: Error processing item #{item.number}: {e}") + continue + finally: + checked_count += 1 + except Exception as e: + print(f"Error fetching items from repository: {e}") + print("This might be due to API rate limits or permissions issues.") + return [] + + if not candidates: + return [] + + # Second pass: calculate all similarities at once + try: + # Build corpus: current text + all candidate texts + corpus = [current_text] + [text for _, _, text in candidates] + + # Fit vectorizer once on entire corpus + vectorizer = TfidfVectorizer( + min_df=1, + stop_words='english', + ngram_range=(1, 2) + ) + tfidf_matrix = vectorizer.fit_transform(corpus) + + # Compute similarities between current item (index 0) and all candidates + similarities = cosine_similarity(tfidf_matrix[0:1], tfidf_matrix[1:])[0] + + # Build results list with items meeting threshold + similar_items = [] + for i, (num, title, _) in enumerate(candidates): + similarity = float(similarities[i]) + if similarity >= self.config['similarity_threshold']: + similar_items.append((num, title, similarity)) + + except Exception as e: + print(f"Error calculating similarities: {e}") + return [] + + # Sort by similarity (highest first) + similar_items.sort(key=lambda x: x[2], reverse=True) + return similar_items + + def add_label(self, item_number: int, label: str, item_type: str = 'issue'): + """Add a label to an issue or PR.""" + try: + # Ensure label exists + try: + self.repo.get_label(label) + except GithubException: + # Create label if it doesn't exist (label not found) + if label == self.config['label_possible_duplicate']: + self.repo.create_label(label, "FFA500", "Potential duplicate issue") + elif label == self.config['label_exact_duplicate']: + self.repo.create_label(label, "FF0000", "Exact duplicate issue") + + if item_type == 'issue': + item = self.repo.get_issue(item_number) + else: + item = self.repo.get_pull(item_number) + + item.add_to_labels(label) + print(f"Added label '{label}' to {item_type} #{item_number}") + except Exception as e: + print(f"Error adding label: {e}") + + def add_comment(self, item_number: int, similar_items: List[Tuple[int, str, float]], + item_type: str = 'issue'): + """Add a comment about potential duplicates.""" + if not similar_items: + return + + item_type_name = "issue" if item_type == 'issue' else "pull request" + + # Build comment message + comment = f"👋 **Potential Duplicate Detected**\n\n" + comment += f"This {item_type_name} appears to be similar to existing {item_type_name}s:\n\n" + + max_to_show = self.config['max_similar_to_show'] + for number, title, similarity in similar_items[:max_to_show]: + similarity_pct = int(similarity * 100) + comment += f"- #{number}: {title} (Similarity: {similarity_pct}%)\n" + + comment += f"\n---\n" + comment += f"Please review these {item_type_name}s to see if any of them address your concern. " + comment += f"If this is indeed a duplicate, please close this {item_type_name} and continue the discussion in the existing one.\n\n" + comment += f"If this is **not** a duplicate, please add more context to help differentiate it.\n\n" + comment += f"*This is an automated message. If you believe this is incorrect, please remove the label and mention a maintainer.*" + + try: + if item_type == 'issue': + item = self.repo.get_issue(item_number) + else: + item = self.repo.get_pull(item_number) + + item.create_comment(comment) + print(f"Added duplicate detection comment to {item_type} #{item_number}") + except Exception as e: + print(f"Error adding comment: {e}") + + def close_item(self, item_number: int, duplicate_of: int, item_type: str = 'issue'): + """Close an item as a duplicate.""" + try: + if item_type == 'issue': + item = self.repo.get_issue(item_number) + else: + item = self.repo.get_pull(item_number) + + comment = f"🔒 **Closing as Exact Duplicate**\n\n" + comment += f"This {item_type} is an exact duplicate of #{duplicate_of}.\n\n" + comment += f"Please continue the discussion in #{duplicate_of}." + + item.create_comment(comment) + item.edit(state='closed') + print(f"Closed {item_type} #{item_number} as duplicate of #{duplicate_of}") + except Exception as e: + print(f"Error closing item: {e}") + + def process_item(self, item_number: int, title: str, body: str, item_type: str = 'issue'): + """Process an issue or PR for duplicate detection.""" + print(f"\n{'='*60}") + print(f"Processing {item_type} #{item_number}: {title}") + print(f"{'='*60}\n") + + # Find similar items + similar_items = self.find_similar_issues(item_number, title, body, item_type) + + if not similar_items: + print(f"✅ No duplicates found for {item_type} #{item_number}") + return + + print(f"\n🔍 Found {len(similar_items)} similar {item_type}(s):") + for num, ttl, sim in similar_items: + print(f" - #{num}: {ttl[:60]}... (Similarity: {sim:.2%})") + + # Get the highest similarity + highest_similarity = similar_items[0][2] + highest_similar_number = similar_items[0][0] + + # Determine action based on similarity + if highest_similarity >= self.config['high_similarity_threshold']: + print(f"\n⚠️ High similarity detected ({highest_similarity:.2%})") + self.add_label(item_number, self.config['label_exact_duplicate'], item_type) + self.add_comment(item_number, similar_items, item_type) + + # Only auto-close issues; PRs should not be auto-closed by this flag + if item_type == 'issue' and self.config['auto_close_exact_match']: + self.close_item(item_number, highest_similar_number, item_type) + else: + print(f"\n⚠️ Possible duplicate detected ({highest_similarity:.2%})") + self.add_label(item_number, self.config['label_possible_duplicate'], item_type) + self.add_comment(item_number, similar_items, item_type) + + +def main(): + """Main entry point.""" + parser = argparse.ArgumentParser(description='Detect duplicate issues and PRs') + parser.add_argument('--type', choices=['issue', 'pr'], required=True, + help='Type of item to check (issue or pr)') + parser.add_argument('--config', default='.github/duplicate-detector-config.yml', + help='Path to configuration file') + args = parser.parse_args() + + # Get environment variables + token = os.getenv('GITHUB_TOKEN') + repo_name = os.getenv('REPOSITORY') + + if not token or not repo_name: + print("Error: GITHUB_TOKEN and REPOSITORY environment variables are required") + sys.exit(1) + + # Get item details based on type + if args.type == 'issue': + item_number = int(os.getenv('ISSUE_NUMBER', 0)) + item_title = os.getenv('ISSUE_TITLE', '').strip() + item_body = os.getenv('ISSUE_BODY', '').strip() + else: + item_number = int(os.getenv('PR_NUMBER', 0)) + item_title = os.getenv('PR_TITLE', '').strip() + item_body = os.getenv('PR_BODY', '').strip() + + if not item_number: + print(f"Error: {args.type.upper()}_NUMBER not found") + sys.exit(1) + + if not item_title: + print(f"Warning: {args.type.upper()}_TITLE is empty") + item_title = f"Untitled {args.type}" + + # Create detector and process + try: + detector = DuplicateDetector(token, repo_name, args.config) + detector.process_item(item_number, item_title, item_body, args.type) + + print(f"\n{'='*60}") + print("✅ Duplicate detection completed successfully") + print(f"{'='*60}\n") + except Exception as e: + print(f"\n{'='*60}") + print(f"❌ Error during duplicate detection: {e}") + print(f"{'='*60}\n") + print("\nThis might be due to:") + print("- GitHub API rate limits") + print("- Insufficient permissions") + print("- Network connectivity issues") + print("- Invalid configuration") + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/.github/scripts/requirements.txt b/.github/scripts/requirements.txt new file mode 100644 index 0000000000..ff83af0482 --- /dev/null +++ b/.github/scripts/requirements.txt @@ -0,0 +1,5 @@ +PyGithub>=2.1.1 +scikit-learn>=1.3.0 +numpy>=1.24.0 +PyYAML>=6.0 +requests>=2.31.0 diff --git a/.github/workflows/duplicate-detector.yml b/.github/workflows/duplicate-detector.yml new file mode 100644 index 0000000000..28db6f2f70 --- /dev/null +++ b/.github/workflows/duplicate-detector.yml @@ -0,0 +1,52 @@ +name: Duplicate Issue and PR Detection + +on: + issues: + types: [opened, reopened] + pull_request_target: + types: [opened, reopened] + +permissions: + issues: write + pull-requests: write + contents: read + +jobs: + detect-duplicates: + runs-on: ubuntu-latest + name: Check for Duplicate Issues and PRs + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + pip install -r .github/scripts/requirements.txt + + - name: Detect duplicates for issues + if: github.event_name == 'issues' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + ISSUE_TITLE: ${{ github.event.issue.title }} + ISSUE_BODY: ${{ github.event.issue.body }} + REPOSITORY: ${{ github.repository }} + run: | + python .github/scripts/detect-duplicates.py --type issue + + - name: Detect duplicates for pull requests + if: github.event_name == 'pull_request_target' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + REPOSITORY: ${{ github.repository }} + run: | + python .github/scripts/detect-duplicates.py --type pr diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 14100d4c1c..7bb3fd68d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,6 +16,10 @@ Apache Fory's website consists of static pages hosted at https://github.com/apac Create an issue with [this form](https://github.com/apache/fory-site/issues/new/choose). +## Automated Duplicate Detection + +This repository uses automated duplicate detection. If your issue is flagged as a potential duplicate, please review the similar issues linked in the bot comment before continuing. + ## How to update doc All updates about docs for [guide](https://github.com/apache/fory/tree/main/docs/guide) and [specification](https://github.com/apache/fory/tree/main/docs/specification) will be synced from [docs in fory repo](https://github.com/apache/fory/tree/main/docs) to this site repo automatically. diff --git a/i18n/en-US/docusaurus-plugin-content-blog/authors.yml b/i18n/en-US/docusaurus-plugin-content-blog/authors.yml new file mode 100644 index 0000000000..0f9be9f8aa --- /dev/null +++ b/i18n/en-US/docusaurus-plugin-content-blog/authors.yml @@ -0,0 +1,23 @@ +chaokunyang: + name: Shawn Yang + title: Apache Fory PMC Chair + url: https://github.com/chaokunyang + image_url: /img/authors/chaokunyang.png + +wangweipeng: + name: Weipeng Wang + title: Apache Fory PMC Member + url: https://github.com/theweipeng + image_url: /img/authors/wangweipeng.png + +liangliangsui: + name: Liangliang Sui + title: Apache Fory Committer + url: https://github.com/LiangliangSui + image_url: /img/authors/liangliangsui.png + +pandalee99: + name: Pan Li + title: Apache Fory Committer + url: https://github.com/pandalee99 + image_url: /img/authors/pandalee99.png diff --git a/i18n/en-US/docusaurus-plugin-content-blog/options.json b/i18n/en-US/docusaurus-plugin-content-blog/options.json new file mode 100644 index 0000000000..e19dda4ea0 --- /dev/null +++ b/i18n/en-US/docusaurus-plugin-content-blog/options.json @@ -0,0 +1,14 @@ +{ + "title": { + "message": "Blog", + "description": "The title for the blog used in SEO" + }, + "description": { + "message": "Blog", + "description": "The description for the blog used in SEO" + }, + "sidebar.title": { + "message": "All our posts", + "description": "The label for the left sidebar" + } +} diff --git a/i18n/en-US/docusaurus-plugin-content-docs/current.json b/i18n/en-US/docusaurus-plugin-content-docs/current.json new file mode 100644 index 0000000000..d1294f208e --- /dev/null +++ b/i18n/en-US/docusaurus-plugin-content-docs/current.json @@ -0,0 +1,18 @@ +{ + "version.label": { + "message": "dev", + "description": "The label for version current" + }, + "sidebar.docsSidebar.category.Introduction": { + "message": "Introduction", + "description": "The label for category Introduction in sidebar docsSidebar" + }, + "sidebar.docsSidebar.category.Start": { + "message": "Start", + "description": "The label for category Start in sidebar docsSidebar" + }, + "sidebar.docsSidebar.category.Guide": { + "message": "Guide", + "description": "The label for category Guide in sidebar docsSidebar" + } +} diff --git a/i18n/en-US/docusaurus-plugin-content-docs/current/.keep b/i18n/en-US/docusaurus-plugin-content-docs/current/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/i18n/en-US/docusaurus-theme-classic/footer.json b/i18n/en-US/docusaurus-theme-classic/footer.json new file mode 100644 index 0000000000..29e520385f --- /dev/null +++ b/i18n/en-US/docusaurus-theme-classic/footer.json @@ -0,0 +1,54 @@ +{ + "link.title.Community": { + "message": "Community", + "description": "The title of the footer links column with title=Community in the footer" + }, + "link.title.Docs": { + "message": "Docs", + "description": "The title of the footer links column with title=Docs in the footer" + }, + "link.title.Repositories": { + "message": "Repositories", + "description": "The title of the footer links column with title=Repositories in the footer" + }, + "link.item.label.Mailing list": { + "message": "Mailing list", + "description": "The label of footer link with label=Mailing list linking to https://lists.apache.org/list.html?dev@fory.apache.org" + }, + "link.item.label.Slack": { + "message": "Slack", + "description": "The label of footer link with label=Slack linking to https://join.slack.com/t/fory-project/shared_invite/zt-1u8soj4qc-ieYEu7ciHOqA2mo47llS8A" + }, + "link.item.label.Twitter": { + "message": "Twitter", + "description": "The label of footer link with label=Twitter linking to https://twitter.com/ApacheFory" + }, + "link.item.label.Install": { + "message": "Install", + "description": "The label of footer link with label=Install linking to /docs/start/install" + }, + "link.item.label.Usage": { + "message": "Usage", + "description": "The label of footer link with label=Usage linking to /docs/start/usage" + }, + "link.item.label.Benchmark": { + "message": "Benchmark", + "description": "The label of footer link with label=Benchmark linking to /docs/introduction/benchmark" + }, + "link.item.label.Fory": { + "message": "Apache Fory™", + "description": "The label of footer link with label=Fory linking to https://github.com/apache/fory" + }, + "link.item.label.Website": { + "message": "Website", + "description": "The label of footer link with label=Website linking to https://github.com/apache/fory-site" + }, + "copyright": { + "message": "
\n

Copyright © 2025 The Apache Software Foundation, Licensed under the Apache License, Version 2.0.
\n Apache Fory, Fory, Apache, the Apache Logo and the Apache Fory logo are either registered trademarks or trademarks of the Apache Software Foundation in the United States and/or other countries.\n

\n
", + "description": "The footer copyright" + }, + "logo.alt": { + "message": "Apache Incubator logo", + "description": "The alt text of footer logo" + } +} diff --git a/i18n/en-US/docusaurus-theme-classic/navbar.json b/i18n/en-US/docusaurus-theme-classic/navbar.json new file mode 100644 index 0000000000..ef6622d754 --- /dev/null +++ b/i18n/en-US/docusaurus-theme-classic/navbar.json @@ -0,0 +1,78 @@ +{ + "logo.alt": { + "message": "Apache Fory™ Logo", + "description": "The alt text of navbar logo" + }, + "item.label.Start": { + "message": "Start", + "description": "Navbar item with label Start" + }, + "item.label.Introduction": { + "message": "Introduction", + "description": "Navbar item with label Introduction" + }, + "item.label.Guide": { + "message": "Guide", + "description": "Navbar item with label Guide" + }, + "item.label.Specification": { + "message": "Specification", + "description": "Navbar item with label Specification" + }, + "item.label.Community": { + "message": "Community", + "description": "Navbar item with label Community" + }, + "item.label.Download": { + "message": "Download", + "description": "Navbar item with label Download" + }, + "item.label.Blog": { + "message": "Blog", + "description": "Navbar item with label Blog" + }, + "item.label.ASF": { + "message": "ASF", + "description": "Navbar item with label ASF" + }, + "item.label.Foundation": { + "message": "Foundation", + "description": "Navbar item with label Foundation" + }, + "item.label.License": { + "message": "License", + "description": "Navbar item with label License" + }, + "item.label.Events": { + "message": "Events", + "description": "Navbar item with label Events" + }, + "item.label.Privacy": { + "message": "Privacy", + "description": "Navbar item with label Privacy" + }, + "item.label.Security": { + "message": "Security", + "description": "Navbar item with label Security" + }, + "item.label.Sponsorship": { + "message": "Sponsorship", + "description": "Navbar item with label Sponsorship" + }, + "item.label.Thanks": { + "message": "Thanks", + "description": "Navbar item with label Thanks" + }, + "item.label.Code of Conduct": { + "message": "Code of Conduct", + "description": "Navbar item with label Code of Conduct" + }, + "item.label.Docs": { + "message": "Docs", + "description": "Navbar item with label Docs" + }, + "item.label.Users": { + "message": "Users", + "description": "Navbar item with label Users" + } +} diff --git a/src/constants/index.ts b/src/constants/index.ts index 4c8ee93d40..d48da91ea6 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,139 +1,139 @@ -export const COPY_SUCCESS_MSG = "Copied!"; -export const COPY_FAIL_MSG = "Failed to copy!"; -export const COPY_TIMEOUT = 2000; - -export const CODE_EXAMPLES = { - java: { - label: "Java", - code: `import java.util.List; -import java.util.Arrays; -import org.apache.fory.*; - -public class Example { - // Note that Fory instances should be reused between - // multiple serializations of different objects. - static ThreadSafeFory fory = Fory.builder().withLanguage(Language.JAVA) - // Allow to deserialize objects unknown types, - // more flexible but less secure. - // .requireClassRegistration(false) - .buildThreadSafeFory(); - - static { - // Registering types can reduce class name serialization - // overhead but not mandatory. - // If secure mode enabled - //all custom types must be registered. - fory.register(SomeClass.class); - } - - public static void main(String[] args) { - SomeClass object = new SomeClass(); - byte[] bytes = fory.serialize(object); - System.out.println(fory.deserialize(bytes)); - } -}`, - }, - kotlin: { - label: "Kotlin", - code: `import org.apache.fory.Fory -import org.apache.fory.ThreadSafeFory -import org.apache.fory.serializer.kotlin.KotlinSerializers - -data class Person(val name: String, val id: Long, val github: String) -data class Point(val x : Int, val y : Int, val z : Int) - -fun main(args: Array) { - // Note: following fory init code should be executed only once in a global scope instead - // of initializing it everytime when serialization. - val fory: ThreadSafeFory = Fory.builder().requireClassRegistration(true).buildThreadSafeFory() - KotlinSerializers.registerSerializers(fory) - fory.register(Person::class.java) - fory.register(Point::class.java) - - val p = Person("Shawn Yang", 1, "https://github.com/chaokunyang") - println(fory.deserialize(fory.serialize(p))) - println(fory.deserialize(fory.serialize(Point(1, 2, 3)))) -}`, - - }, - scala: { - label: "Scala", - code: `case class Person(name: String, id: Long, github: String) -case class Point(x : Int, y : Int, z : Int) - -object ScalaExample { - val fory: Fory = Fory.builder().withScalaOptimizationEnabled(true).build() - // Register optimized fory serializers for scala - ScalaSerializers.registerSerializers(fory) - fory.register(classOf[Person]) - fory.register(classOf[Point]) - - def main(args: Array[String]): Unit = { - val p = Person("Shawn Yang", 1, "https://github.com/chaokunyang") - println(fory.deserialize(fory.serialize(p))) - println(fory.deserialize(fory.serialize(Point(1, 2, 3)))) - } -}`, - - }, - rust: { - label: "Rust", - code: `use fory::{Fory, Error}; -use fory::ForyObject; - -#[derive(ForyObject, Debug, PartialEq)] -struct User { - name: String, - age: i32, - email: String, -} - -fn main() -> Result<(), Error> { - let mut fory = Fory::default(); - fory.register::(1)?; - - let user = User { name: "Alice".into(), age: 30, email: "alice@example.com".into() }; - let bytes = fory.serialize(&user)?; - let decoded: User = fory.deserialize(&bytes)?; - assert_eq!(user, decoded); - Ok(()) -}`, - - }, - - python: { - label: "Python", - code: `import pyfory -from dataclasses import dataclass -from typing import List, Dict - -@dataclass -class Person: - name: str - age: int - scores: List[int] - metadata: Dict[str, str] - -# Python mode - supports all Python types including dataclasses -fory = pyfory.Fory(xlang=False, ref=True) -fory.register(Person) -person = Person("Bob", 25, [88, 92, 85], {"team": "engineering"}) -data = fory.serialize(person) -result = fory.deserialize(data) -print(result) # Person(name='Bob', age=25, ...)`, - }, - -}; - -export const imageUrls = [ - { key: "java", src: "/home/java.svg", label: "Java" }, - { key: "python", src: "/home/python.svg", label: "Python" }, - { key: "golang", src: "/home/golang.svg", label: "Golang" }, - { - key: "javascript", - src: "/home/JavaScript.svg", - label: "JavaScript", - }, - { key: "rust", src: "/home/Rust.svg", label: "Rust" }, - { key: "more", src: "/home/more.svg", label: "More" }, -]; +export const COPY_SUCCESS_MSG = "Copied!"; +export const COPY_FAIL_MSG = "Failed to copy!"; +export const COPY_TIMEOUT = 2000; + +export const CODE_EXAMPLES = { + java: { + label: "Java", + code: `import java.util.List; +import java.util.Arrays; +import org.apache.fory.*; + +public class Example { + // Note that Fory instances should be reused between + // multiple serializations of different objects. + static ThreadSafeFory fory = Fory.builder().withLanguage(Language.JAVA) + // Allow to deserialize objects unknown types, + // more flexible but less secure. + // .requireClassRegistration(false) + .buildThreadSafeFory(); + + static { + // Registering types can reduce class name serialization + // overhead but not mandatory. + // If secure mode enabled + //all custom types must be registered. + fory.register(SomeClass.class); + } + + public static void main(String[] args) { + SomeClass object = new SomeClass(); + byte[] bytes = fory.serialize(object); + System.out.println(fory.deserialize(bytes)); + } +}`, + }, + kotlin: { + label: "Kotlin", + code: `import org.apache.fory.Fory +import org.apache.fory.ThreadSafeFory +import org.apache.fory.serializer.kotlin.KotlinSerializers + +data class Person(val name: String, val id: Long, val github: String) +data class Point(val x : Int, val y : Int, val z : Int) + +fun main(args: Array) { + // Note: following fory init code should be executed only once in a global scope instead + // of initializing it everytime when serialization. + val fory: ThreadSafeFory = Fory.builder().requireClassRegistration(true).buildThreadSafeFory() + KotlinSerializers.registerSerializers(fory) + fory.register(Person::class.java) + fory.register(Point::class.java) + + val p = Person("Shawn Yang", 1, "https://github.com/chaokunyang") + println(fory.deserialize(fory.serialize(p))) + println(fory.deserialize(fory.serialize(Point(1, 2, 3)))) +}`, + + }, + scala: { + label: "Scala", + code: `case class Person(name: String, id: Long, github: String) +case class Point(x : Int, y : Int, z : Int) + +object ScalaExample { + val fory: Fory = Fory.builder().withScalaOptimizationEnabled(true).build() + // Register optimized fory serializers for scala + ScalaSerializers.registerSerializers(fory) + fory.register(classOf[Person]) + fory.register(classOf[Point]) + + def main(args: Array[String]): Unit = { + val p = Person("Shawn Yang", 1, "https://github.com/chaokunyang") + println(fory.deserialize(fory.serialize(p))) + println(fory.deserialize(fory.serialize(Point(1, 2, 3)))) + } +}`, + + }, + rust: { + label: "Rust", + code: `use fory::{Fory, Error}; +use fory::ForyObject; + +#[derive(ForyObject, Debug, PartialEq)] +struct User { + name: String, + age: i32, + email: String, +} + +fn main() -> Result<(), Error> { + let mut fory = Fory::default(); + fory.register::(1)?; + + let user = User { name: "Alice".into(), age: 30, email: "alice@example.com".into() }; + let bytes = fory.serialize(&user)?; + let decoded: User = fory.deserialize(&bytes)?; + assert_eq!(user, decoded); + Ok(()) +}`, + + }, + + python: { + label: "Python", + code: `import pyfory +from dataclasses import dataclass +from typing import List, Dict + +@dataclass +class Person: + name: str + age: int + scores: List[int] + metadata: Dict[str, str] + +# Python mode - supports all Python types including dataclasses +fory = pyfory.Fory(xlang=False, ref=True) +fory.register(Person) +person = Person("Bob", 25, [88, 92, 85], {"team": "engineering"}) +data = fory.serialize(person) +result = fory.deserialize(data) +print(result) # Person(name='Bob', age=25, ...)`, + }, + +}; + +export const imageUrls = [ + { key: "java", src: "/home/java.svg", label: "Java" }, + { key: "python", src: "/home/python.svg", label: "Python" }, + { key: "golang", src: "/home/golang.svg", label: "Golang" }, + { + key: "javascript", + src: "/home/JavaScript.svg", + label: "JavaScript", + }, + { key: "rust", src: "/home/Rust.svg", label: "Rust" }, + { key: "more", src: "/home/more.svg", label: "More" }, +]; diff --git a/src/hooks/useAOS.tsx b/src/hooks/useAOS.tsx index c202c06feb..ddbd783ac5 100644 --- a/src/hooks/useAOS.tsx +++ b/src/hooks/useAOS.tsx @@ -1,31 +1,31 @@ -import { useEffect } from "react"; -import AOS from "aos"; - -// 提取 AOS 初始化配置常量 -const AOS_CONFIG = { - offset: 100, - duration: 700, - easing: "ease-out-quad", - once: true, -}; - -const useAOS = () => { - useEffect(() => { - try { - // 初始化 AOS - AOS.init(AOS_CONFIG); - // 监听页面加载完成事件,刷新 AOS - const loadHandler = () => AOS.refresh(); - window.addEventListener("load", loadHandler); - - // 组件卸载时移除事件监听器,避免内存泄漏 - return () => { - window.removeEventListener("load", loadHandler); - }; - } catch (error) { - console.error("AOS 初始化出错:", error); - } - }, []); -}; - -export default useAOS; +import { useEffect } from "react"; +import AOS from "aos"; + +// 提取 AOS 初始化配置常量 +const AOS_CONFIG = { + offset: 100, + duration: 700, + easing: "ease-out-quad", + once: true, +}; + +const useAOS = () => { + useEffect(() => { + try { + // 初始化 AOS + AOS.init(AOS_CONFIG); + // 监听页面加载完成事件,刷新 AOS + const loadHandler = () => AOS.refresh(); + window.addEventListener("load", loadHandler); + + // 组件卸载时移除事件监听器,避免内存泄漏 + return () => { + window.removeEventListener("load", loadHandler); + }; + } catch (error) { + console.error("AOS 初始化出错:", error); + } + }, []); +}; + +export default useAOS; diff --git a/src/pages/home/css/tailwind.css b/src/pages/home/css/tailwind.css index d25c2e7203..18cebfc245 100644 --- a/src/pages/home/css/tailwind.css +++ b/src/pages/home/css/tailwind.css @@ -1,47 +1,47 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - - -.border { - border-width: 1px; - border-style: solid; -} - -.border-gray-400 { - border-color: rgba(156, 163, 175, 0.3); -} - -/* custom-preflight.css */ -/* 设置全局的盒模型 */ -html { - box-sizing: border-box; - -webkit-text-size-adjust: 100%; -} - -*, -*::before, -*::after { - box-sizing: inherit; -} - -/* 重置 body 样式 */ -body { - margin: 0; - font-family: 'Inter', sans-serif; - font-size: 1rem; - line-height: 1.5; -} - -/* 重置按钮样式 */ -button { - background-color: transparent; - background-image: none; - font-family: inherit; - font-size: 100%; - font-weight: inherit; - line-height: inherit; - color: inherit; - margin: 0; - padding: 0; +@tailwind base; +@tailwind components; +@tailwind utilities; + + +.border { + border-width: 1px; + border-style: solid; +} + +.border-gray-400 { + border-color: rgba(156, 163, 175, 0.3); +} + +/* custom-preflight.css */ +/* 设置全局的盒模型 */ +html { + box-sizing: border-box; + -webkit-text-size-adjust: 100%; +} + +*, +*::before, +*::after { + box-sizing: inherit; +} + +/* 重置 body 样式 */ +body { + margin: 0; + font-family: 'Inter', sans-serif; + font-size: 1rem; + line-height: 1.5; +} + +/* 重置按钮样式 */ +button { + background-color: transparent; + background-image: none; + font-family: inherit; + font-size: 100%; + font-weight: inherit; + line-height: inherit; + color: inherit; + margin: 0; + padding: 0; } \ No newline at end of file