norman

Opinionated Desktop Interface to manage Research Papers to Notion Database

https://github.com/klaffenboeck/norman

Science Score: 31.0%

This score indicates how likely this project is to be science-related based on various indicators:

  • CITATION.cff file
    Found CITATION.cff file
  • codemeta.json file
    Found codemeta.json file
  • .zenodo.json file
  • DOI references
  • Academic publication links
  • Academic email domains
  • Institutional organization owner
  • JOSS paper metadata
  • Scientific vocabulary similarity
    Low similarity (12.5%) to scientific vocabulary
Last synced: 10 months ago · JSON representation ·

Repository

Opinionated Desktop Interface to manage Research Papers to Notion Database

Basic Info
  • Host: GitHub
  • Owner: klaffenboeck
  • License: gpl-3.0
  • Language: Python
  • Default Branch: main
  • Homepage:
  • Size: 70.3 KB
Statistics
  • Stars: 0
  • Watchers: 1
  • Forks: 0
  • Open Issues: 0
  • Releases: 0
Created over 1 year ago · Last pushed over 1 year ago
Metadata Files
Readme License Citation Authors

readme.md

NoRMan (Notion Reference Manager)

NoRMan is a desktop application designed for managing academic references stored in Notion. Originally built to streamline my own workflow, it is currently tailored to my specific use case and may not yet be broadly applicable to others. However, I recognize its potential value beyond my personal needs and plan to continue its development to make it more flexible and generally useful.

Features

  • BibTeX Parsing: Import references from BibTeX files into your Notion database.
  • Notion Synchronization: Load and manage references from Notion within NoRMan.
  • Citation Formatting: Generate in-text citations and bibliographies in multiple formats:
    • HTML
    • LaTeX
    • Markdown
    • RichText (RTF, Word, etc.)
  • Customizable Styles: Define and modify citation styles using configurable templates.

Installation

Installation instructions will be provided as development progresses.

Usage

  1. Sync with Notion: Connect NoRMan to your Notion database to push or retrieve references.
  2. Generate Citations: Format references for use in HTML, LaTeX, Markdown, or RichText documents.
  3. BibTeX Parsing: Extract and structure reference fields for searchability.
  4. Generate Consistent IDs: Ensure uniform citation keys for your BibTeX files.

Detailed instructions and examples will be added soon.

Development Status

NoRMan is in early development, and its functionality is still evolving. As of now, it is highly customized for my own needs and may not yet be suitable for general use. Over time, I aim to refine its features and improve its adaptability.

Roadmap

Will be added as the project progresses.

License

NoRMan is licensed under the GNU General Public License v3.0. See the LICENSE file for details.

Owner

  • Name: Manfred Klaffenboeck
  • Login: klaffenboeck
  • Kind: user

Citation (citation_manager.py)

import json
import re
import bibtexparser
from authors import Author, AuthorList
from config_handler import *
from bibtex_formatter import *
from output_formatter import *
from collections import Counter
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

__all__ = ["CitationManager"]

class CitationManager:
    def __init__(self, config_path: str = "configs/config.json", style_config: str = "formatting_styles.json"):
        self.original_bibtex_file = ""
        self.modified_bibtex_file = ""
        self.original_bibtex_key = ""
        self.bibtex_url = ""
        self.bibtex_authors = ""
        self.bibtex_doi = ""
        self.year = ""
        #self.title = ""
        self.journal = ""
        self.abstract = ""
        self.venue = ""
        self.authors = None
        self.bibtex_data = None
        self.config_path = config_path
        self.style_config_path = style_config
        self.style_config_data = ConfigHandler.load_config(self.style_config_path)
        self.citation_key = ""
        self.papertrail = ""
        self.projects = []
        self.count = ""
        self.type = ""
        self.notes = ""
        self.bibtex_formatter = None
        self.output_formatter = HtmlCssFormatter()
        self._title = "" #self.title
        self._short_title_length = None  # Default short title length
        self._short_title = None  # Default short title    def __init__(self, title: str):
        self._short_title_set_manually = False

        self.stopwords = set(stopwords.words('english'))  # Load stopwords


    key = property(lambda self: self.citation_key)
    cite_count = property(lambda self: self.count)

    @property
    def title(self):
        return self._title

    # NOTE: title.setter not tested yet regarding _short_title_set_manually
    @title.setter
    def title(self, new_title: str):
        if self._title == new_title:
            return None
        self._title = new_title
        self.create_short_title()

    @property
    def short_title(self):
        return self._short_title

    @short_title.setter
    def short_title(self, new_short_title: str):
        self._short_title = new_short_title
        self._short_title_length = len(new_short_title.split())
        self._short_title_set_manually = True

    @property
    def short_title_length(self):
        return self._short_title_length

    @short_title_length.setter
    def short_title_length(self, length: int):
        self._short_title_length = length
        self.create_short_title(fixed_length=length)

    def reload_config(self):
        """Reloads the style configuration from the JSON file."""
        self.style_config_data = ConfigHandler.load_config(self.style_config_path)

    def parse_bibtex(self, bibtex):
        self.original_bibtex_file = bibtex
        self.bibtex_data = bibtexparser.loads(bibtex)

        for entry in self.bibtex_data.entries:
            if "ID" in entry:
                self.original_bibtex_key = entry["ID"]
            if "year" in entry:
                self.year = entry["year"]
            if "title" in entry:
                self.title = entry["title"]
            if "journal" in entry:
                self.journal = entry["journal"]
            elif "booktitle" in entry:
                self.journal = entry["booktitle"]
            if "abstract" in entry:
                self.abstract = entry["abstract"]
                del entry["abstract"]
            if "url" in entry:
                self.bibtex_url = entry["url"].strip()
            if "author" in entry:
                self.bibtex_authors = entry["author"]
                self.authors = AuthorList(entry["author"], self.config_path)
            if "doi" in entry:
                self.bibtex_doi = entry["doi"].strip()
            if "eprint" in entry:
                del entry["eprint"]

        self.bibtex_data.entries[0]["ID"] = self.get_citation_key()

        self.match_venue()
        self.set_link_doi()
        self.modified_bibtex_file = bibtexparser.dumps(self.bibtex_data)
        self.bibtex_formatter = BibtexFormatter(self.modified_bibtex_file)

    def set_link_doi(self):
        if self.bibtex_url:
            self.link_doi = self.bibtex_url
        elif self.bibtex_doi:
            match = re.search(r"(?:doi\\.org/|^/)(.+)", self.bibtex_doi)
            if match:
                self.link_doi = "https://doi.org/" + match.group(1)
            else:
                self.link_doi = "https://doi.org/" + self.bibtex_doi

    def match_venue(self):
        if not self.venue:
            return
        config_data = ConfigHandler.load_config(self.config_path)
        for mapping in config_data.get("venue_mapping", []):
            pattern = mapping.get("regex", "")
            if pattern and re.search(pattern, self.journal):
                self.venue = mapping.get("venue-mapping", "")
                return

    def generate_citation_key(self):
        fa = self.authors.first_author()
        self.citation_key = fa.get_citation_key(self.year)
        return self.citation_key

    def set_citation_key(self, key):
        self.citation_key = key

    def get_citation_key(self):
        return self.citation_key if self.citation_key else self.generate_citation_key()

    def create_short_title(self, fixed_length: int = None, **kwargs) -> str:
        if self._short_title_set_manually and self.short_title:
            return self.short_title
        self._short_title_length = fixed_length if fixed_length is not None else kwargs.get('max_words', 5)

        min_words = kwargs.get('min_words', 4)
        max_words = kwargs.get('max_words', 5)

        # Handle colons and dashes (prefer first part)
        title = re.split(r'[:\-]', self._title)[0].strip()

        # Tokenize and filter stopwords
        words = word_tokenize(title)
        content_words = [w for w in words if w.lower() not in self.stopwords and w.isalnum()]

        # Keep acronyms and capitalized words
        important_words = [w for w in content_words if w.istitle() or w.isupper()]
        if len(important_words) < min_words:
            important_words = content_words  # Fall back to all content words

        # Determine length based on title length
        title_length = len(title)
        if title_length <= 6:
            short_title = title  # Keep full title if 6 words or fewer
        else:
            dynamic_length = max(min_words, min(max_words, title_length // 2))
            short_title = important_words[:dynamic_length]

        if fixed_length is not None:
            short_title = important_words[:fixed_length]
        else:
            short_title = important_words[:dynamic_length]

        if short_title:
            short_title[0] = short_title[0].capitalize()

        self._short_title = ' '.join(short_title)
        self._short_title_length = len(short_title)
        self._short_title_set_manually = False
        return self._short_title


    def get(self, key, *args, **kwargs):
        print(f"Resolving {key} with args={args} and kwargs={kwargs}")
        if '.' in key:
            obj, method = key.split('.', 1)
            print(f"Object: {obj}, Method: {method}")

            obj_ref = getattr(self, obj, None)
            if obj_ref:
                print(f"Found object: {obj_ref}")
                if hasattr(obj_ref, method):
                    print(f"Calling method: {method} on {obj}")
                    return getattr(obj_ref, method)(*args, **kwargs)
                else:
                    print(f"Method {method} not found on {obj}")
            else:
                print(f"Object {obj} not found in CitationManager")

        formatter = kwargs.get("formatter", None)
        if formatter:
            value = getattr(self, key, '')
            # HACK: should actually take the object
            value = formatter.format_key(key, value)
            return value
        else:
            value = getattr(self, key, '')
            print(f"Returning direct attribute: {value}")
            return value

    def select_style_template(self, parameters: dict) -> str:
        style = parameters.get("style", "APA")
        encoding = parameters.get("encoding", "default")  # Reserved for future use
        bib_type = parameters.get("bib_type", "default")  # E.g., article, book, inproceedings

        style_data = self.style_config_data.get("journal_formatting_styles", {}).get(style, {})
        type_data = style_data.get(bib_type, style_data.get("default", {}))

        template = type_data.get("reference", "")
        return template

    def process_citation(self, parameters: dict):
        print("CALLED PROCESS_CITATION")
        template_str = self.select_style_template(parameters)

        def replace_conditionals(text):
            pattern = re.compile(r'\{\{(.*?)((?:##.*?##|\{\{.*?\}\})+)(.*?)\}\}', re.DOTALL)

            def conditional_replacer(match):
                prefix = match.group(1)
                content = match.group(2)
                suffix = match.group(3)

                markers = re.findall(r'##(.*?)##', content)

                if all(self.get(marker.strip(), formatter=self.output_formatter) for marker in markers):
                    content = replace_conditionals(content)
                    for marker in markers:
                        content = content.replace(f'##{marker}##', self.get(marker.strip(), formatter=self.output_formatter))
                    return f"{prefix}{content}{suffix}"
                return ''

            return pattern.sub(conditional_replacer, text)

        def replace_variables(text):
            pattern = re.compile(r'##(.*?)##')

            def variable_replacer(match):
                key = match.group(1).strip()
                return self.get(key, formatter=self.output_formatter)

            return pattern.sub(variable_replacer, text)

        def postprocess(text):
            # Check for final marker {{{.}}} and extract the desired final symbol
            final_marker_match = re.search(r'\{\{\{(.*?)\}\}\}$', text)
            final_symbol = final_marker_match.group(1) if final_marker_match else ''

            # Remove final marker from the text
            text = re.sub(r'\{\{\{.*?\}\}\}$', '', text)

            # Replace trailing non-alphanumeric characters, preserving ), ], >
            text = re.sub(r'([^\w\)\]\>]+)$', '', text)

            # Append the final symbol if it exists
            if final_symbol:
                if text.endswith((')', ']', '>')):
                    text = text[:-1] + final_symbol + text[-1]
                else:
                    text += final_symbol

            if self.output_formatter:
                return self.output_formatter.format_final_entry(text, id=self.get_citation_key())
            else:
                return text

        processed = replace_conditionals(template_str)
        processed = replace_variables(processed)
        processed = postprocess(processed)
        return processed

GitHub Events

Total
  • Push event: 14
  • Create event: 1
Last Year
  • Push event: 14
  • Create event: 1

Dependencies

requirements.txt pypi
  • PyPDF2 ==3.0.1
  • PyYAML ==6.0.2
  • Pygments ==2.19.1
  • Unidecode ==1.3.8
  • altgraph ==0.17.4
  • asttokens ==3.0.0
  • beautifulsoup4 ==4.13.3
  • bibtexparser ==1.4.3
  • certifi ==2024.12.14
  • charset-normalizer ==3.4.1
  • decorator ==5.1.1
  • executing ==2.2.0
  • html5lib ==1.1
  • idna ==3.10
  • ipdb ==0.13.13
  • ipython ==8.32.0
  • jedi ==0.19.2
  • latexcodec ==3.0.0
  • lxml ==5.3.1
  • macholib ==1.16.3
  • matplotlib-inline ==0.1.7
  • mistune ==3.1.1
  • modulegraph ==0.19.6
  • packaging ==24.2
  • parso ==0.8.4
  • pexpect ==4.9.0
  • pip-autoremove ==0.10.0
  • prompt_toolkit ==3.0.50
  • ptyprocess ==0.7.0
  • pure_eval ==0.2.3
  • py2app ==0.28.8
  • pybtex ==0.24.0
  • pyinstaller ==6.11.1
  • pyinstaller-hooks-contrib ==2025.0
  • pylatexenc ==2.10
  • pyparsing ==3.2.1
  • regex ==2024.11.6
  • requests ==2.32.3
  • setuptools ==75.8.0
  • six ==1.17.0
  • soupsieve ==2.6
  • stack-data ==0.6.3
  • tkinterdnd2-universal ==1.7.3
  • traitlets ==5.14.3
  • typing_extensions ==4.12.2
  • urllib3 ==2.3.0
  • wcwidth ==0.2.13
  • webencodings ==0.5.1