-
\ No newline at end of file
diff --git a/archives/nasal_api_doc-py2-2019.1.1.html b/archives/nasal_api_doc-py2-2019.1.1.html
deleted file mode 100644
index edfcd84..0000000
--- a/archives/nasal_api_doc-py2-2019.1.1.html
+++ /dev/null
@@ -1,5513 +0,0 @@
- Nasal API
-
Nasal $FGROOT Library Flightgear version: 2019.1.1
This file is generated automatically by scripts/python/nasal_api_doc.py
-
\ No newline at end of file
diff --git a/download-latest-fgdata.bat b/download-latest-fgdata.bat
new file mode 100644
index 0000000..372bf2e
--- /dev/null
+++ b/download-latest-fgdata.bat
@@ -0,0 +1,19 @@
+REM This Windows script downloads latest files from GitLab: Nasal folder, and the version file.
+
+@echo off
+
+if exist FGROOT (
+ rmdir /S /Q FGROOT
+)
+
+mkdir FGROOT
+cd FGROOT || exit /b
+
+git clone --filter=blob:none --depth 1 --no-checkout https://gitlab.com/flightgear/fgdata.git FGDATA
+cd FGDATA || exit /b
+
+git sparse-checkout init --no-cone
+git sparse-checkout set /Nasal /version
+git checkout next
+
+rmdir /S /Q .git
diff --git a/download-latest-fgdata.sh b/download-latest-fgdata.sh
new file mode 100644
index 0000000..d480f8f
--- /dev/null
+++ b/download-latest-fgdata.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# This Linux script downloads latest files from GitLab: Nasal folder, and the version file.
+
+if [ -d "FGROOT" ]; then
+ rm -rf FGROOT
+fi
+
+mkdir FGROOT
+cd FGROOT || exit
+
+git clone --filter=blob:none --depth 1 --no-checkout https://gitlab.com/flightgear/fgdata.git FGDATA
+cd FGDATA || exit
+
+git sparse-checkout init --no-cone
+git sparse-checkout set /Nasal /version
+git checkout next
+
+rm -rf .git
diff --git a/nasal_api_doc.py b/nasal_api_doc.py
deleted file mode 100644
index 777e014..0000000
--- a/nasal_api_doc.py
+++ /dev/null
@@ -1,324 +0,0 @@
-#!/usr/bin/env python3
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2012 Adrian Musceac
-# Copyright (C) 2019-2025 RenanMsV
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see .
-
-"""Script which generates an API documentation file for Nasal libraries
-located inside $FGROOT/Nasal/"""
-
-import os
-import sys
-import re
-import time
-import argparse
-import glob
-
-########### Local $FGROOT/Nasal/ path ##########
-NASAL_PATH="../fgfs/fgdata/Nasal/"
-OUTPUT_PATH="./out"
-
-def get_files(nasal_dir):
- ''' Scans for all nasal files and generates documents '''
- if nasal_dir[-1]!='/':
- nasal_dir+='/'
- try:
- os.stat(nasal_dir)
- except FileNotFoundError:
- print("The path does not exist:", nasal_dir)
- sys.exit(1)
- fgroot_dir = nasal_dir.rstrip('/').replace('Nasal','')
- f_version = open(fgroot_dir+'version','r',encoding='utf-8')
- version = f_version.read(256).rstrip('\n')
- top_level = []
- modules = []
- top_namespaces = []
- files_list = os.listdir(nasal_dir)
- for f in files_list:
- if f.find(".nas")!=-1:
- top_level.append(f)
- continue
- if os.path.isdir(nasal_dir + f):
- modules.append(f)
- top_level.sort()
- modules.sort()
- if len(top_level) ==0:
- print("This does not look like the correct $FGROOT/Nasal path")
- sys.exit()
- if len(modules)==0:
- print("Warning: could not find any submodules")
- for f in top_level:
- namespace=f.replace(".nas","")
- functions=parse_file(nasal_dir + f)
- top_namespaces.append([namespace,functions])
- for m in modules:
- files=glob.glob(nasal_dir+m+"/*.nas")
- for f in files:
- functions=parse_file(f)
- top_namespaces.append([m,functions])
-
- output_text(top_namespaces,modules,version)
-
-
-def output_text(top_namespaces,modules,version):
- ''' Ouputs the text, namespaces, modules to html files '''
- try:
- if not os.path.exists(OUTPUT_PATH):
- os.mkdir(OUTPUT_PATH)
- except NotADirectoryError as e:
- print("Could not create output directory:", OUTPUT_PATH, e)
- sys.exit(1)
- timestring = time.strftime("%m-%d-%Y %I-%M-%S%p")
- fw=open(OUTPUT_PATH + 'nasal_api_doc-' + version + '.html','wb')
- buf='\
- Nasal API - ' + version + '\
- \n\
- '
-
- buf+='
\
- Nasal $FGROOT Library Flightgear version: '+version+'\
- This file is generated automatically by nasal_api_doc.py at ' + timestring + '\
-
\n'
- done=[]
- for namespace in top_namespaces:
- color='0000cc'
- if namespace[0] in modules:
- color='cc0000'
- if namespace[0] not in done:
- buf+=''+namespace[0]+' \n'
- done.append(namespace[0])
- buf+='
\n'
- done2=[]
- for namespace in top_namespaces:
- if namespace[0] not in done2:
- buf+='
\n'
- for comment in functions[2]:
- if comment.find('=====')!=-1:
- buf+=''
- else:
- tempComment = comment.replace('#','').replace('<','<').replace('>','>')
- if tempComment.strip()!="":
- buf+= '
'+tempComment+'
\n'
- buf+='
\n'
- if namespace[0] not in done2:
- buf+='
\n'
- buf+=''
- fw.write(buf.encode('utf8'))
- fw.close()
- print("Generated file", fw.name)
-
-
-def parse_file(filename):
- ''' Parses a nasal file to search for keywords and docstrings '''
- fr=open(filename,'r', encoding='utf-8')
- content=fr.readlines()
- i=0
- retval=[]
- classname=""
- for line in content:
- match=re.search(r'^var\s+([A-Za-z0-9_-]+)\s*=\s*func\s*\(?([A-Za-z0-9_\s,=.\n-]*)\)?',line)
- if match is not None:
- func_name=match.group(1)
- comments=[]
- param=match.group(2)
- if(line.find(')')==-1 and line.find('(')!=-1):
- k=i+1
- while(content[k].find(')')==-1):
- param+=content[k].rstrip('\n')
- k+=1
- param+=content[k].split(')')[0]
- j=i-1
- count=0
- while ( j>i-35 and j>-1):
- if count>3:
- break
- if len(content[j])<2:
- j-=1
- count+=1
- continue
- if re.search(r'^\s*#',content[j]) is not None:
- comments.append(content[j].rstrip('\n'))
- j-=1
- else:
- break
- if(len(comments)>1):
- comments.reverse()
- retval.append((func_name, param,comments))
- i+=1
- continue
-
- match3=re.search(r'^var\s*([A-Za-z0-9_-]+)\s*=\s*{\s*(\n|})',line)
- if match3 is not None:
- classname=match3.group(1)
-
- comments=[]
-
- j=i-1
- count=0
- while ( j>i-35 and j>-1):
- if count>3:
- break
- if len(content[j])<2:
- j-=1
- count+=1
- continue
- if re.search(r'^\s*#',content[j]) is not None:
- comments.append(content[j].rstrip('\n'))
- j-=1
- else:
- break
- if(len(comments)>1):
- comments.reverse()
- retval.append((classname+'.', '',comments))
- i+=1
- continue
-
- match2=re.search(r'^\s*([A-Za-z0-9_-]+)\s*:\s*func\s*\(?([A-Za-z0-9_\s,=.\n-]*)\)?',line)
- if match2 is not None:
- func_name=match2.group(1)
- comments=[]
- param=match2.group(2)
- if(line.find(')')==-1 and line.find('(')!=-1):
- k=i+1
- while(content[k].find(')')==-1):
- param+=content[k].rstrip('\n')
- k+=1
- param+=content[k].split(')')[0]
- j=i-1
- count=0
- while ( j>i-35 and j>-1):
- if count>3:
- break
- if len(content[j])<2:
- j-=1
- count+=1
- continue
- if re.search(r'^\s*#',content[j]) is not None:
- comments.append(content[j].rstrip('\n'))
- j-=1
- else:
- break
- if(len(comments)>1):
- comments.reverse()
- if classname =='':
- continue
- retval.append((classname+'.'+func_name, param,comments))
- i+=1
- continue
-
- match4=re.search(r'^([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)\s*=\s*func\s*\(?([A-Za-z0-9_\s,=\n.-]*)\)?',line)
- if match4 is not None:
- classname=match4.group(1)
- func_name=match4.group(2)
- comments=[]
- param=match4.group(3)
- if(line.find(')')==-1 and line.find('(')!=-1):
- k=i+1
- while(content[k].find(')')==-1):
- param+=content[k].rstrip('\n')
- k+=1
- param+=content[k].split(')')[0]
- j=i-1
- count=0
- while ( j>i-35 and j>-1):
- if count>3:
- break
- if len(content[j])<2:
- j-=1
- count+=1
- continue
- if re.search(r'^\s*#',content[j]) is not None:
- comments.append(content[j].rstrip('\n'))
- j-=1
- else:
- break
- if(len(comments)>1):
- comments.reverse()
- retval.append((classname+'.'+func_name, param,comments))
- i+=1
- continue
-
- i+=1
- return retval
-
-
-if __name__ == "__main__":
- parser = argparse.ArgumentParser(
- description="Auto generates Nasal API documentation from FlightGear's nasal scripts."
- )
-
- parser.add_argument(
- "-f", "--fg-root",
- metavar="PATH",
- help="The desired FlightGear data folder (defaults to ../fgfs/fgdata/Nasal/).",
- default="../fgfs/fgdata/Nasal/",
- )
-
- parser.add_argument(
- "-o", "--output",
- metavar="PATH",
- help="The desired output folder (defaults to the script folder).",
- default="./out",
- )
-
- if len(sys.argv) == 1: # Show help if no arguments
- parser.print_help()
- sys.exit()
-
- args = parser.parse_args()
-
- NASAL_PATH = args.fg_root
- OUTPUT_PATH = args.output
- if OUTPUT_PATH[-1] != '/':
- OUTPUT_PATH += '/'
-
- get_files(nasal_dir=NASAL_PATH)
diff --git a/nasal_api_docs/__init__.py b/nasal_api_docs/__init__.py
new file mode 100644
index 0000000..a4646f2
--- /dev/null
+++ b/nasal_api_docs/__init__.py
@@ -0,0 +1,41 @@
+# Copyright (C) 2012 Adrian Musceac
+# Copyright (C) 2019-2026 RenanMsV
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+Nasal API Docs
+==============
+
+A Python package for parsing and generating documentation for FlightGear Nasal scripts.
+
+Typical usage example:
+ from nasal_api_docs import NasalAPI
+
+ nasal_api = NasalAPI(fg_root_dir="/path/to/FGRoot", output_dir="output/")
+ nasal_api.generate_all()
+ nasal_api.generate_html()
+"""
+
+# nasal_api_docs/__init__.py
+from importlib.metadata import version
+
+__version__ = version("nasal_api_docs")
+
+from .nasalapi import NasalAPI
+from .logger import get_logger
+
+logger = get_logger()
+
+__all__ = ["NasalAPI", "logger"]
diff --git a/nasal_api_docs/__main__.py b/nasal_api_docs/__main__.py
new file mode 100644
index 0000000..523061c
--- /dev/null
+++ b/nasal_api_docs/__main__.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2012 Adrian Musceac
+# Copyright (C) 2019-2026 RenanMsV
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+Main entry point for the Nasal API documentation generator.
+
+Parses FlightGear Nasal scripts and generates documentation in multiple formats.
+Supports command-line arguments for FlightGear data folder and output path.
+
+Usage:
+ python -m nasal_api_docs -f /path/to/fgdata/ -o ./out
+"""
+
+import argparse
+import sys
+from pathlib import Path
+
+from nasal_api_docs import NasalAPI
+from .logger import get_logger
+
+DEFAULT_NASAL_PATH = None
+DEFAULT_OUTPUT_PATH = Path("./out")
+
+
+def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
+ """Parses command-line arguments."""
+ parser = argparse.ArgumentParser(
+ prog="nasal-api-docs",
+ description=(
+ "Auto-generate Nasal API documentation from FlightGear Nasal scripts."
+ )
+ )
+ parser.add_argument(
+ "-f",
+ metavar="PATH",
+ help="Path to the FlightGear Data folder.",
+ required=True
+ )
+ parser.add_argument(
+ "-o",
+ metavar="PATH",
+ help=f"Output folder (default: {DEFAULT_OUTPUT_PATH}).",
+ default=str(DEFAULT_OUTPUT_PATH),
+ )
+
+ args = parser.parse_args(argv)
+ return args
+
+
+def main(argv: list[str] | None = None) -> int:
+ """Runs the Nasal API documentation generator."""
+ logger = get_logger()
+
+ args = _parse_args(argv or sys.argv[1:])
+ fg_root_dir = Path(args.f)
+ output_dir = Path(args.o)
+
+ try:
+ nasal_api = NasalAPI(fg_root_dir, output_dir)
+
+ fg_version = nasal_api.get_fg_version()
+ logger.info("Found FlightGear version %s", fg_version)
+
+ html_file = nasal_api.generate_html()
+ logger.info("Generated HTML at %s", html_file)
+
+ json_file = nasal_api.generate_json_tree()
+ logger.info("Generated JSON tree at %s", json_file)
+
+ except FileNotFoundError as e:
+ logger.error("Missing file or directory: %s", e)
+ raise
+ except ValueError as e:
+ logger.error("Invalid value: %s", e)
+ raise
+ except Exception as e: # pylint: disable=broad-exception-caught
+ logger.error("Unexpected error: %s", e)
+ raise
+
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/nasal_api_docs/filesystem.py b/nasal_api_docs/filesystem.py
new file mode 100644
index 0000000..bd8c7af
--- /dev/null
+++ b/nasal_api_docs/filesystem.py
@@ -0,0 +1,179 @@
+# Copyright (C) 2012 Adrian Musceac
+# Copyright (C) 2019-2026 RenanMsV
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Filesystem utilities for the Nasal API documentation generator.
+
+Provides representations for Nasal source files and functions, as well as
+recursive scanning and version detection for FlightGear data trees.
+
+Classes:
+ NasalFunction: Represents a Nasal function or class.
+ NasalItem: Represents a file or module in the Nasal hierarchy.
+ NasalFileSystem: Scans the file system and builds the in-memory tree.
+"""
+
+from __future__ import annotations
+from dataclasses import dataclass, field
+
+
+from pathlib import Path
+from typing import Any, Dict, List
+
+from .parser import NasalParser
+from .logger import get_logger
+logger = get_logger()
+
+
+@dataclass
+class NasalFunction:
+ """Represents a Nasal function with name, args, and comments."""
+ name: str
+ args: List[str]
+ comments: List[str]
+ type: str = "" # computed in __post_init__
+
+ def __post_init__(self):
+ self.type = "class_definition" if self.name.endswith(".") else "function"
+ self.name = self.name.rstrip(".")
+
+ def to_dict(self) -> Dict[str, Any]:
+ """
+ Converts this object to a dictionary suitable for JSON.
+
+ Returns:
+ dict: The object as a dict.
+ """
+ return {
+ "type": self.type,
+ "name": self.name,
+ "args": self.args,
+ "comments": self.comments,
+ }
+
+
+@dataclass
+class NasalItem:
+ """Represents a file or module in the Nasal hierarchy."""
+ name: str
+ path: Path
+ root_path: Path
+ is_module: bool = False
+ children: list["NasalItem"] = field(default_factory=lambda: []) # submodules
+ functions: list["NasalFunction"] = field(default_factory=lambda: []) # files
+ icon: str = "" # computed in __post_init__
+ rel_path: str = "" # computed in __post_init__
+ id: str = "" # computed in __post_init__
+ type: str = "" # computed in __post_init__
+
+ def __post_init__(self):
+ self.icon = "📁" if self.is_module else "📄" # π, π
+ self.rel_path = (
+ (
+ "Nasal\\"
+ + str((self.path / self.name).relative_to(self.root_path))
+ + ("\\" if self.is_module else ".nas")
+ )
+ .replace("\\", "/")
+ )
+ self.id = self.rel_path[6:].lower().replace("/", "_").rstrip("_")
+ logger.info("Found Nasal item: %s, id: %s", self.rel_path, self.id)
+
+ def to_dict(self) -> Dict[str, Any]:
+ """
+ Converts this object to a dictionary suitable for JSON.
+
+ Returns:
+ dict: The object as a dict.
+ """
+ return {
+ "name": self.name,
+ "path": str(self.path),
+ "rel_path": self.rel_path,
+ "is_module": self.is_module,
+ "icon": self.icon,
+ "functions": [
+ f.to_dict()
+ for f in self.functions
+ ],
+ "children": [child.to_dict() for child in self.children],
+ }
+
+
+class NasalFileSystem:
+ """_summary_
+ """
+ _parser: NasalParser = NasalParser()
+ nasal_tree: List[NasalItem]
+ nasal_dir: Path
+ fg_root_dir: Path
+ fg_version: str
+
+ def __init__(self, fg_root_dir: Path):
+ """Inits the Nasal file system."""
+ self.fg_root_dir = fg_root_dir
+ self.nasal_dir = fg_root_dir / "Nasal"
+ self.fg_version = self._read_fg_version()
+ self.nasal_tree = self._get_nasal_tree()
+
+ def _read_fg_version(self) -> str:
+ """Read FlightGear version string from the $FGROOT 'version' file."""
+ version_file = self.fg_root_dir / "version"
+ if not version_file.exists():
+ raise FileNotFoundError("Version file not found in $FGROOT")
+ with version_file.open("r", encoding="utf-8", errors="replace") as f:
+ self.fg_version = f.read(256).rstrip("\n")
+ return self.fg_version
+
+ def _get_nasal_tree(self) -> List[NasalItem]:
+ """
+ Scan the nasal_dir recursively and return a list of NasalItems (files/modules).
+ """
+ self.nasal_dir = self.nasal_dir.resolve()
+ if not self.nasal_dir.exists():
+ raise FileNotFoundError(f"Path does not exist: {self.nasal_dir}")
+
+ def scan_dir(path: Path) -> List[NasalItem]:
+ items: List[NasalItem] = []
+ for entry in sorted(path.iterdir()):
+ if entry.is_file() and entry.suffix == ".nas":
+ file_item = NasalItem(
+ name=entry.stem,
+ path=path,
+ root_path=self.nasal_dir
+ )
+ # Convert tuples from parse_file() to NasalFunction objects
+ file_item.functions = [
+ NasalFunction(
+ name=f[0],
+ args=[a.strip() for a in f[1].split(',') if a.strip()],
+ comments=f[2]
+ )
+ for f in self._parser.parse_file(entry)
+ ]
+ items.append(file_item)
+ elif entry.is_dir():
+ module_item = NasalItem(
+ name=entry.name,
+ is_module=True,
+ path=path,
+ root_path=self.nasal_dir
+ )
+ # Recursively scan subfolder
+ module_item.children = scan_dir(entry)
+ items.append(module_item)
+ return items
+
+ return scan_dir(self.nasal_dir)
diff --git a/nasal_api_docs/generator.py b/nasal_api_docs/generator.py
new file mode 100644
index 0000000..7fbc835
--- /dev/null
+++ b/nasal_api_docs/generator.py
@@ -0,0 +1,138 @@
+# Copyright (C) 2012 Adrian Musceac
+# Copyright (C) 2019-2026 RenanMsV
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Documentation generator for Nasal API.
+
+This module uses Jinja2 templates and structured data from NasalFileSystem
+to produce various output formats such as HTML, JSON, Markdown, and CSV.
+"""
+
+import json
+from datetime import datetime
+from pathlib import Path
+from typing import List
+from platform import python_version as pl_python_version
+
+from jinja2 import Environment, FileSystemLoader, select_autoescape
+from . import __version__ # pylint: disable=cyclic-import
+from .filesystem import NasalFileSystem
+from .parser import NasalParser
+
+TEMPLATE_DIR = Path(__file__).parent / "templates"
+
+
+class NasalDocsGenerator:
+ """Generate Nasal API documentation files in multiple formats.
+
+ Attributes:
+ file_system (NasalFileSystem): Access to parsed Nasal tree and metadata.
+ output_dir (Path): Destination folder for generated documentation files.
+ """
+
+ _file_system: NasalFileSystem
+ output_dir: Path
+
+ def __init__(self, file_system: NasalFileSystem, output_dir: Path):
+ """Initialize the documentation generator."""
+ self._file_system = file_system
+ self.output_dir = output_dir
+
+ def _get_timestamp_str(self) -> str:
+ """Return a formatted timestamp for output metadata."""
+ return datetime.now().strftime("%m-%d-%Y %I-%M-%S%p")
+
+ def generate_all(self) -> List[Path]:
+ """Generate all supported documentation formats.
+
+ Returns:
+ list[Path]: Paths of all generated output files.
+ """
+ files: List[Path] = []
+ files.append(self.generate_html())
+ files.append(self.generate_json_tree())
+ return files
+
+ def generate_html(self) -> Path:
+ """Generate the HTML documentation file from the Nasal tree.
+
+ Returns:
+ Path: The generated HTML file path.
+ """
+ self.output_dir.mkdir(parents=True, exist_ok=True)
+ out_file = (
+ self.output_dir / f"nasal_api_doc-{self._file_system.fg_version}.html"
+ )
+ timestamp = self._get_timestamp_str()
+ package_version = __version__
+ parser_version = NasalParser.VERSION_STR
+ python_version = pl_python_version()
+
+ env = Environment(
+ loader=FileSystemLoader(TEMPLATE_DIR / "html"),
+ autoescape=select_autoescape(["html"]),
+ trim_blocks=True,
+ lstrip_blocks=True,
+ )
+
+ template = env.get_template("docs_html_template.j2")
+ html_content = template.render(
+ fg_version=self._file_system.fg_version,
+ version=package_version,
+ parser_version=parser_version,
+ python_version=python_version,
+ timestamp=timestamp,
+ tree=self._file_system.nasal_tree,
+ )
+
+ out_file.write_text(html_content, encoding="utf-8")
+ return out_file
+
+ def generate_json_tree(self):
+ """Generate a JSON representation of the Nasal API tree.
+
+ Returns:
+ Path: Path to the generated JSON file.
+ """
+ filename = f"json_tree-{self._file_system.fg_version}.json"
+ path = self.output_dir / filename
+ with path.open("w", encoding="utf-8") as f:
+ timestamp = self._get_timestamp_str()
+ fg_version = self._file_system.fg_version
+ package_version = __version__
+ parser_version = NasalParser.VERSION_STR
+ python_version = pl_python_version()
+ json.dump(
+ {
+ "meta": {
+ "timestamp": timestamp,
+ "package_version": package_version,
+ "parser_version": parser_version,
+ "fg_version": fg_version,
+ "python_version": python_version
+ },
+ "data": [item.to_dict() for item in self._file_system.nasal_tree],
+ },
+ f,
+ indent=2,
+ ensure_ascii=False,
+ )
+ return path
+
+ def generate_markdown(self):
+ """Generate Markdown documentation (not yet implemented)."""
+
+ def generate_csv(self):
+ """Generate CSV documentation (not yet implemented)."""
diff --git a/nasal_api_docs/logger.py b/nasal_api_docs/logger.py
new file mode 100644
index 0000000..2d9de48
--- /dev/null
+++ b/nasal_api_docs/logger.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2012 Adrian Musceac
+# Copyright (C) 2019-2026 RenanMsV
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Package-wide logger for nasal_api_docs."""
+
+import logging
+
+
+def get_logger() -> logging.Logger:
+ """
+ Returns a configured logger for the package.
+
+ Returns:
+ logging.Logger: Configured logger instance.
+ """
+ logger = logging.getLogger("nasal_api_docs")
+ if not logger.hasHandlers():
+ handler = logging.StreamHandler()
+ formatter = logging.Formatter("%(levelname)s: %(message)s")
+ handler.setFormatter(formatter)
+ logger.addHandler(handler)
+ logger.setLevel(logging.INFO)
+ return logger
diff --git a/nasal_api_docs/nasalapi.py b/nasal_api_docs/nasalapi.py
new file mode 100644
index 0000000..881fa12
--- /dev/null
+++ b/nasal_api_docs/nasalapi.py
@@ -0,0 +1,110 @@
+# Copyright (C) 2012 Adrian Musceac
+# Copyright (C) 2019-2026 RenanMsV
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Nasal API main interface.
+
+This module provides a high-level object-oriented interface for parsing
+FlightGear Nasal scripts and generating documentation in multiple formats.
+
+Classes:
+ NasalAPI: Main entry point for interacting with the Nasal parser, filesystem,
+ and documentation generators.
+"""
+
+from pathlib import Path
+
+from .generator import NasalDocsGenerator
+from .parser import NasalParser
+from .filesystem import NasalFileSystem
+
+
+class NasalAPI:
+ """High-level interface for the Nasal documentation generator.
+
+ This class serves as the main entry point to interact with the package.
+ It encapsulates:
+ - File discovery and parsing via NasalFileSystem
+ - Documentation generation via NasalDocsGenerator
+ - Access to FlightGear metadata such as version and structure
+
+ Attributes:
+ fg_root_dir (Path): Path to the FlightGear root data directory ($FG_ROOT).
+ output_dir (Path): Directory where output files will be written.
+ """
+
+ _file_system: NasalFileSystem
+ _generator: NasalDocsGenerator
+ _parser: NasalParser
+
+ fg_root_dir: Path
+ output_dir: Path
+
+ def __init__(self, fg_root_dir: Path, output_dir: Path):
+ """Initialize the Nasal API interface.
+
+ Args:
+ fg_root_dir (Path): The FlightGear root data directory.
+ output_dir (Path): The directory to output generated documentation files.
+ """
+ self.fg_root_dir = fg_root_dir
+ self.output_dir = output_dir
+
+ self._file_system = NasalFileSystem(self.fg_root_dir)
+ self._generator = NasalDocsGenerator(self._file_system, self.output_dir)
+
+ def get_fg_version(self) -> str:
+ """Return the FlightGear version detected from $FG_ROOT."""
+ return self._file_system.fg_version
+
+ def generate_html(self):
+ """Generate the Nasal API documentation in HTML format.
+
+ Returns:
+ Path: The path of the generated HTML file.
+ """
+ return self._generator.generate_html()
+
+ def generate_json_tree(self):
+ """Generate the Nasal API tree in JSON format.
+
+ Returns:
+ Path: The path of the generated JSON file.
+ """
+ return self._generator.generate_json_tree()
+
+ def generate_markdown(self):
+ """Generate the Nasal API documentation in Markdown format.
+
+ Returns:
+ Path: The path of the generated Markdown file.
+ """
+ return self._generator.generate_markdown()
+
+ def generate_csv(self):
+ """Generate the Nasal API documentation in CSV format.
+
+ Returns:
+ Path: The path of the generated CSV file.
+ """
+ return self._generator.generate_csv()
+
+ def generate_all(self):
+ """Generate all supported documentation formats.
+
+ Returns:
+ list[Path]: List of all generated file paths.
+ """
+ return self._generator.generate_all()
diff --git a/nasal_api_docs/parser.py b/nasal_api_docs/parser.py
new file mode 100644
index 0000000..77ae1c3
--- /dev/null
+++ b/nasal_api_docs/parser.py
@@ -0,0 +1,180 @@
+# Copyright (C) 2012 Adrian Musceac
+# Copyright (C) 2019-2026 RenanMsV
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Nasal parser for FlightGear scripts.
+
+This module uses regular expressions to extract documentation-relevant
+constructs from Nasal source files.
+
+Extracts:
+ - Function definitions (top-level and member functions)
+ - Class declarations
+ - Dot-assigned functions (Class.func = func(...))
+ - Comments immediately preceding definitions
+
+Returns:
+ list[tuple[str, str, list[str]]]: A structured list of definitions,
+ each containing the symbol name, parameter string, and preceding comments.
+"""
+
+import re
+from pathlib import Path
+from typing import List, Tuple
+
+
+# Regex patterns
+_RE_VAR_CLASS = re.compile(r"^var\s*([A-Za-z0-9_-]+)\s*=\s*{\s*(\n|})")
+_RE_VAR_FUNC = re.compile(
+ r"^var\s+([A-Za-z0-9_-]+)\s*=\s*func\s*\(?([A-Za-z0-9_\s,=.\n-]*)\)?"
+)
+_RE_MEMBER_FUNC = re.compile(
+ r"^\s*([A-Za-z0-9_-]+)\s*:\s*func\s*\(?([A-Za-z0-9_\s,=.\n-]*)\)?"
+)
+_RE_DOT_FUNC = re.compile(
+ r"^([A-Za-z0-9_-]+)\.([A-Za-z0-9_-]+)\s*=\s*func\s*\(?([A-Za-z0-9_\s,=\n.-]*)\)?"
+)
+
+
+class NasalParser:
+ """Parse Nasal source files into structured documentation data."""
+
+ VERSION = (1, 0, 0)
+ VERSION_STR = "1.0.0"
+
+ _CHANGELOG = {
+ (1, 0, 0): "Initial release"
+ }
+
+ @classmethod
+ def version_info(cls) -> str:
+ """Returns the Parser version info as a string."""
+ notes = cls._CHANGELOG.get(cls.VERSION, "")
+ return f"Parser v{cls.VERSION_STR} β {notes}"
+
+ def parse_file(self, filename: Path) -> List[Tuple[str, str, List[str]]]:
+ """Parse a Nasal source file.
+
+ Args:
+ filename (Path): Path to the Nasal (.nas) source file.
+
+ Returns:
+ list[tuple[str, str, list[str]]]: A list of tuples containing:
+ - The symbol name (e.g. class.func)
+ - The raw parameter list string
+ - The preceding comment lines
+ """
+ with filename.open("r", encoding="utf-8", errors="replace") as f:
+ lines = f.readlines()
+
+ result: List[Tuple[str, str, List[str]]] = []
+ classname = ""
+
+ for i, line in enumerate(lines):
+ # var func
+ match = _RE_VAR_FUNC.match(line)
+ if match:
+ func_name = match.group(1)
+ param = self._expand_multiline_params(lines, i, match.group(2))
+ comments = self._collect_comments(lines, i)
+ result.append((func_name, param, comments))
+ continue
+
+ # var class
+ match = _RE_VAR_CLASS.match(line)
+ if match:
+ classname = match.group(1)
+ comments = self._collect_comments(lines, i)
+ result.append((classname + ".", "", comments))
+ continue
+
+ # member func inside class
+ match = _RE_MEMBER_FUNC.match(line)
+ if match and classname:
+ func_name = match.group(1)
+ param = self._expand_multiline_params(lines, i, match.group(2))
+ comments = self._collect_comments(lines, i)
+ result.append((classname + "." + func_name, param, comments))
+ continue
+
+ # dot function: Class.func = func(...)
+ match = _RE_DOT_FUNC.match(line)
+ if match:
+ classname = match.group(1)
+ func_name = match.group(2)
+ param = self._expand_multiline_params(lines, i, match.group(3))
+ comments = self._collect_comments(lines, i)
+ result.append((classname + "." + func_name, param, comments))
+ continue
+
+ return result
+
+ def _expand_multiline_params(self, lines: list[str], index: int, param: str) -> str:
+ """Expand parameter lists that span multiple lines.
+
+ Args:
+ lines (list[str]): The source file lines.
+ index (int): The current line index where parsing started.
+ param (str): The initial parameter string captured.
+
+ Returns:
+ str: The complete parameter string.
+ """
+ if "(" in lines[index] and ")" not in lines[index]:
+ k = index + 1
+ while k < len(lines) and ")" not in lines[k]:
+ param += lines[k].rstrip("\n")
+ k += 1
+ if k < len(lines):
+ param += lines[k].split(")")[0]
+ return param
+
+ def _collect_comments(self, lines: list[str], index: int) -> list[str]:
+ """Collect contiguous preceding comments above a line.
+
+ Scans backward from the given index and collects lines that start with `#`,
+ skipping up to 128 blank lines or 255 total lines.
+
+ Args:
+ lines (list[str]): The source file lines.
+ index (int): Index of the current definition line.
+
+ Returns:
+ list[str]: Cleaned comment lines in correct order.
+ """
+ comments: list[str] = []
+ empty_count = 0
+
+ for j in range(index - 1, max(index - 255, -1), -1):
+ line = lines[j]
+ stripped = line.strip()
+
+ if len(stripped) < 2:
+ empty_count += 1
+ if empty_count > 128:
+ break
+ continue
+
+ if stripped.startswith("#"):
+ clean_comment = stripped.lstrip("#").strip()
+ if clean_comment:
+ comments.append(clean_comment)
+ else:
+ break
+
+ if len(comments) > 1:
+ comments.reverse()
+
+ return comments
diff --git a/nasal_api_docs/templates/csv/.gitkeep b/nasal_api_docs/templates/csv/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/nasal_api_docs/templates/html/docs_html_template.css b/nasal_api_docs/templates/html/docs_html_template.css
new file mode 100644
index 0000000..6d2f5e9
--- /dev/null
+++ b/nasal_api_docs/templates/html/docs_html_template.css
@@ -0,0 +1,129 @@
+:root {
+ /* light mode colors 'lt' */
+ --white-color-lt: #fff;
+ --white-color-light-lt: #eee;
+ --purple-lt: #555588;
+ --purple-light-lt: #8888AC;
+ --purple-bold-lt: #383870;
+ --class-definition-lt: #322b2b;
+ --function-lt: var(--purple-bold-lt);
+ --args-lt: #b11515;
+ --comments-lt: #000;
+ --background-lt: var(--white-color-lt);
+
+ /* dark mode color 'dk' */
+ --white-color-dk: #e6e6e6;
+ --white-color-light-dk: #222239;
+ --purple-dk: #555588;
+ --purple-light-dk: #8888AC;
+ --purple-bold-dk: #bcbcff;
+ --class-definition-dk: #ddd;
+ --function-dk: var(--purple-bold-dk);
+ --args-dk: #75bbcf;
+ --comments-dk: var(--white-color-dk);
+ --background-dk: var(--white-color-light-dk);
+
+ /* current applied colors are here */
+ --white-color: var(--white-color-lt);
+ --white-color-light: var(--white-color-light-lt);
+ --purple: var(--purple-lt);
+ --purple-light: var(--purple-light-lt);
+ --purple-bold: var(--purple-bold-lt);
+ --class-definition: var(--class-definition-lt);
+ --function: var(--purple-bold-lt);
+ --args: var(--args-lt);
+ --comments: var(--comments-lt);
+ --background: var(--background-lt);
+}
+
+body {
+ font-family: 'Fira Sans', helvetica, arial, sans-serif;
+ width: 1024px;
+ background-color: var(--background);
+}
+.page_title {
+ padding-left:20px;
+ display:block;
+ color:var(--white-color);
+ background-color: var(--purple);
+}
+.page_title span {
+ font-size: 14px;
+}
+.info_links {
+ background-color: var(--purple);
+ color: var(--white-color);
+ padding: 4px 4px;
+ border: none;
+ border-radius: 1px;
+ font-size: 16px;
+ cursor: pointer;
+ transition: background-color 0.3s, transform 0.2s;
+}
+.info_links:hover {
+ background-color: var(--purple-light);
+ transform: scale(1.02);
+}
+.info_links a {
+ text-decoration: none;
+ color: var(--white-color);
+}
+.right_namespace_menu {
+ float:right;
+}
+.right_namespace_menu h2 {
+ font-size:14px;
+ height:450px;
+ width:300px;
+ overflow:scroll;
+ display:block;
+ position:fixed;
+ top:20px;
+ right:20px;
+ background-color:var(--purple);
+ border:4px solid var(--purple-light);
+}
+.main_module_link {
+ margin-left:10px;
+ color:var(--white-color);
+ text-decoration: none;
+}
+.container {
+ background-color:var(--white-color-light);
+ clear:left;
+ margin-top:20px;
+}
+.namespace_title {
+ padding-left:20px;
+ color:var(--white-color);
+ background-color:var(--purple)
+}
+.class_function {
+ padding-left:20px;
+ background-color:var(--white-color-light);
+ color:var(--purple-bold)
+}
+.class_definition {
+ padding-left:20px;
+ background-color:var(--white-color-light);
+ color:var(--purple-bold)
+}
+.class_definition u {
+ color: var(--class-definition);
+}
+.function {
+ padding-left:20px;
+ background-color:var(--white-color-light);
+ color:var(--purple-bold)
+}
+.function span {
+ color: var(--args);
+}
+.comments {
+ padding-left:40px;
+ display:inline;
+ font-size:12px;
+ color: var(--comments);
+}
+.rel_path {font-size: 12px;width: 100px;}
+/* hr {margin-left:30px;margin-right:30px;} */
\ No newline at end of file
diff --git a/nasal_api_docs/templates/html/docs_html_template.j2 b/nasal_api_docs/templates/html/docs_html_template.j2
new file mode 100644
index 0000000..5f6a4e6
--- /dev/null
+++ b/nasal_api_docs/templates/html/docs_html_template.j2
@@ -0,0 +1,102 @@
+
+
+
+ Nasal API - {{ fg_version }}
+
+
+
+
+
+
+ Nasal $FGROOT Library
+
+ FlightGear version: {{ fg_version }} .
+ This file was generated automatically by nasal-api-docs at {{ timestamp }} .
+ nasal-api-docs v{{ version }} | Parser v{{ parser_version }} | Python v{{ python_version }} .
+
+
+{% endmacro %}
+
+ {% for namespace in tree %}
+ {{ render_namespace(namespace) }}
+ {% endfor %}
+
+
\ No newline at end of file
diff --git a/nasal_api_docs/templates/html/toggle_theme_button.j2 b/nasal_api_docs/templates/html/toggle_theme_button.j2
new file mode 100644
index 0000000..f6571ca
--- /dev/null
+++ b/nasal_api_docs/templates/html/toggle_theme_button.j2
@@ -0,0 +1,49 @@
+
+ Theme:
+
+
+
+
+
+
diff --git a/nasal_api_docs/templates/markdown/.gitkeep b/nasal_api_docs/templates/markdown/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..bfec3a4
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,65 @@
+[build-system]
+requires = ["setuptools>=42", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "nasal-api-docs"
+version = "0.2.0"
+dependencies = [
+ "Jinja2>=3.1.6,<4"
+]
+requires-python = ">=3.7"
+authors = [
+ { name="RenanMsV" },
+ { name="Adrian Musceac" },
+]
+maintainers = [
+ { name = "RenanMsV" }
+]
+description = "Auto-generate Nasal API documentation from the FlightGear Nasal folder"
+readme = "README.md"
+license = "GPL-3.0-or-later"
+license-files = ["LICEN[CS]E"]
+keywords = ["flightgear", "nasal", "api", "documentation", "generator"]
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Environment :: Console",
+ "Operating System :: OS Independent",
+ "Intended Audience :: Developers",
+ "Topic :: Documentation",
+ "Topic :: Software Development :: Documentation",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+]
+
+[project.urls]
+Homepage = "https://renanmsv.github.io/nasal-api-docs/"
+Documentation = "https://renanmsv.github.io/nasal-api-docs/latest/"
+Repository = "https://github.com/RenanMsV/nasal-api-docs.git"
+Issues = "https://github.com/RenanMsV/nasal-api-docs/issues"
+Changelog = "https://github.com/RenanMsV/nasal-api-docs/CHANGELOG.md"
+
+[project.scripts]
+nasal-api-docs = "nasal_api_docs.__main__:main"
+
+[tool.setuptools]
+include-package-data = true
+
+[tool.setuptools.package-data]
+"nasal_api_docs" = ["templates/**/*"]
+
+[tool.setuptools.packages.find]
+where = ["."]
+include = ["nasal_api_docs*"]
+
+[tool.pytest.ini_options]
+pythonpath = ["."]
+testpaths = ["tests"]
+addopts = "-sv"
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..d3ea0f3
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,6 @@
+[flake8]
+max-line-length = 88
+
+[pylint]
+max-line-length = 88
+disable = too-few-public-methods, too-many-instance-attributes
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..89ca8f2
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,86 @@
+# Copyright (C) 2012 Adrian Musceac
+# Copyright (C) 2019-2026 RenanMsV
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Shared pytest configuration and fixtures for nasal_api_docs tests."""
+
+from pathlib import Path
+import pytest
+
+from nasal_api_docs import NasalAPI
+
+
+@pytest.fixture(scope="session")
+def fg_root_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
+ """
+ Provides a temporary fake FlightGear root directory with the minimal structure
+ required by tests.
+
+ The layout will be:
+ tmp/
+ βββ fgdata/
+ β βββ Nasal/
+ βββ output/
+
+ Returns:
+ Path: The path to the created fgdata directory (used as fg_root_dir).
+ """
+ tmp_root = tmp_path_factory.mktemp("tmp")
+
+ # Create folder structure
+ fg_dir = tmp_root / "fgdata"
+ nasal_dir = fg_dir / "Nasal"
+
+ fg_dir.mkdir(parents=True, exist_ok=True)
+ nasal_dir.mkdir(parents=True, exist_ok=True)
+
+ # Create a fake FlightGear version file
+ (fg_dir / "version").write_text("9797.1.0", encoding="utf-8")
+
+ # Create a small fake .nas file
+ (nasal_dir / "aircraft.nas").write_text(
+ "# This is a comment first line\n"
+ "# \n"
+ "# This is a comment third line\n"
+ "#\n"
+ "var makeNode = func(n, anotherArgument) {\n"
+ "\tif (isa(n, props.Node))\n"
+ "\t\treturn n;\n"
+ "\telse\n"
+ "\t\treturn props.globals.getNode(n, 1);\n"
+ "}\n",
+ encoding="utf-8",
+ )
+
+ return fg_dir
+
+
+@pytest.fixture(scope="session")
+def output_dir(tmp_path_factory: pytest.TempPathFactory) -> Path:
+ """
+ Provides a temporary output directory for generated documentation.
+
+ Returns:
+ Path: The path to the created temporary output directory.
+ """
+ # Let pytest handle temp directory creation and cleanup
+ out_dir = tmp_path_factory.mktemp("output")
+ return out_dir
+
+
+@pytest.fixture(scope="session")
+def nasal_api(fg_root_dir: Path, output_dir: Path) -> NasalAPI: # pylint: disable=W0621
+ """Provides a ready-to-use NasalAPI instance for all tests."""
+ return NasalAPI(fg_root_dir=fg_root_dir, output_dir=output_dir)
diff --git a/tests/core_test.py b/tests/core_test.py
new file mode 100644
index 0000000..af20740
--- /dev/null
+++ b/tests/core_test.py
@@ -0,0 +1,24 @@
+# Copyright (C) 2012 Adrian Musceac
+# Copyright (C) 2019-2026 RenanMsV
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Core integration tests for the nasal_api_docs package."""
+
+from nasal_api_docs import NasalAPI
+
+
+def test_importable():
+ """Ensure the main class NasalAPI can be imported."""
+ assert NasalAPI is not None
diff --git a/tests/generator_test.py b/tests/generator_test.py
new file mode 100644
index 0000000..dbdb21b
--- /dev/null
+++ b/tests/generator_test.py
@@ -0,0 +1,98 @@
+# Copyright (C) 2012 Adrian Musceac
+# Copyright (C) 2019-2026 RenanMsV
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""HTML generator tests for the nasal_api_docs package."""
+
+import json
+from nasal_api_docs import NasalAPI, parser
+
+
+def test_basic_generation(nasal_api: NasalAPI):
+ """Test that the API can read fg_version and generate documentation."""
+ version = nasal_api.get_fg_version()
+ assert version.startswith("9797"), "Incorrect or missing FG version"
+
+ html_path = nasal_api.generate_html()
+ json_path = nasal_api.generate_json_tree()
+
+ assert html_path.exists(), "HTML output file not created"
+ assert json_path.exists(), "JSON output file not created"
+
+
+def test_html_generation(nasal_api: NasalAPI):
+ """Test that the API generated a reasonable enough html."""
+ html_path = nasal_api.generate_html()
+
+ assert html_path.exists(), "HTML output file not created"
+
+ with open(html_path, "r", encoding="utf-8") as file:
+ data = file.read()
+
+ assert "Nasal API - 9797.1.0" in data, "Incorrect title."
+
+ assert "FlightGear version: 9797.1.0 . " in data, "Incorrect FG version."
+
+ assert (
+ "Plausible.org"
+ ) in data, "Missing link buttons."
+
+ assert (
+ "📄 aircraft"
+ ) in data, "Incorrect module link in right namespace menu."
+
+ assert (
+ "📄 aircraft"
+ ) in data, "Incorrect namespace title."
+
+ assert (
+ " "
+ "Nasal/aircraft.nas"
+ ) in data, "Incorrect path of Nasal file."
+
+ assert (
+ "aircraft.makeNode ( n, "
+ "anotherArgument )"
+ ) in data, "Incorrect function name and parameters."
+
+ assert (
+ "
"
+ ) in data, "Incorrect comment."
+
+
+def test_json_generation(nasal_api: NasalAPI):
+ """Test that the API generated a reasonable enough json."""
+ json_path = nasal_api.generate_json_tree()
+
+ assert json_path.exists(), "JSON output file not created"
+
+ with open(json_path, "r", encoding="utf-8") as file:
+ data = json.load(file)
+
+ assert data["meta"], "Missing metadata"
+
+ assert data["meta"]["fg_version"].startswith("9797"), (
+ "Incorrect or missing FG version."
+ )
+
+ assert data["meta"]["parser_version"] == parser.NasalParser.VERSION_STR, (
+ "Incorrect parser version."
+ )
diff --git a/tests/parser_test.py b/tests/parser_test.py
new file mode 100644
index 0000000..f5ffa2e
--- /dev/null
+++ b/tests/parser_test.py
@@ -0,0 +1,177 @@
+# Copyright (C) 2012 Adrian Musceac
+# Copyright (C) 2019-2026 RenanMsV
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Unit tests for the NasalParser."""
+
+from pathlib import Path
+from typing import List, Tuple
+
+from nasal_api_docs.parser import NasalParser
+
+
+def test_parse_basic_var_function(tmp_path: Path) -> None:
+ """
+ Verify that the parser can detect a simple Nasal function definition.
+
+ Example: var foo = func(...)
+
+ Ensures:
+ - One function definition is detected.
+ - Function name and parameters are parsed correctly.
+ - Correct comments are attached to the parsed result.
+ """
+ file: Path = tmp_path / "simple_var_function.nas"
+ file.write_text(
+ "# hello this is a comment\n"
+ "#\n"
+ "var foo = func(a, b) {\n \n\treturn a + b; \n}\n",
+ encoding="utf-8"
+ )
+
+ parser: NasalParser = NasalParser()
+ result: List[Tuple[str, str, List[str]]] = parser.parse_file(file)
+
+ assert len(result) == 1, "Expected one function definition."
+
+ name: str
+ params: str
+ comments: List[str]
+ name, params, comments = result[0]
+
+ assert name == "foo", "Expected name to be 'foo'."
+ assert params == "a, b", "Missing or invalid params."
+ assert comments == ["hello this is a comment"], (
+ "Expected comments to be a single line. "
+ "The empty comment line should have been ignored."
+ )
+
+
+def test_parse_basic_dot_function(tmp_path: Path) -> None:
+ """
+ Verify that the parser can detect a simple Nasal dot function definition.
+
+ Example: Class.func = func(...)
+
+ Ensures:
+ - One function definition is detected.
+ - Function name and parameters are parsed correctly.
+ - Correct comments are attached to the parsed result.
+ """
+ file: Path = tmp_path / "simple_dot_function.nas"
+ file.write_text(
+ "# hello this is a comment\n"
+ "#\n"
+ "Class.new = func(a, b) {\n"
+ " return a + b;\n"
+ "}\n",
+ encoding="utf-8"
+ )
+
+ parser: NasalParser = NasalParser()
+ result: List[Tuple[str, str, List[str]]] = parser.parse_file(file)
+
+ assert len(result) == 1, "Expected one function definition."
+
+ name: str
+ params: str
+ comments: List[str]
+ name, params, comments = result[0]
+
+ assert name == "Class.new", "Expected name to be 'Class.new'."
+ assert params == "a, b", "Missing or invalid params."
+ assert comments == ["hello this is a comment"], (
+ "Expected comments to be a single line. "
+ "The empty comment line should have been ignored."
+ )
+
+
+def test_parse_basic_var_class(tmp_path: Path) -> None:
+ """
+ Verify that the parser can detect a simple Nasal var class definition.
+
+ Example: var Class = {...}
+
+ Ensures:
+ - One class definition is detected.
+ - Class name is parsed correctly.
+ - Correct comments are attached to the parsed result.
+ """
+ file: Path = tmp_path / "simple_var_class.nas"
+ file.write_text(
+ "# hello this is a comment\n"
+ "#\n"
+ "var Class = {\n"
+ "}\n",
+ encoding="utf-8"
+ )
+
+ parser: NasalParser = NasalParser()
+ result: List[Tuple[str, str, List[str]]] = parser.parse_file(file)
+
+ assert len(result) == 1, "Expected one function definition."
+
+ name: str
+ params: str
+ comments: List[str]
+ name, params, comments = result[0]
+
+ assert name == "Class.", "Expected name to be 'Class.'."
+ assert params == "", "Expected no parameters."
+ assert comments == ["hello this is a comment"], (
+ "Expected comments to be a single line. "
+ "The empty comment line should have been ignored."
+ )
+
+
+def test_parse_basic_member_function(tmp_path: Path) -> None:
+ """
+ Verify that the parser can detect a simple Nasal member function definition.
+
+ Example: var Class = { init: func {...}}
+
+ Ensures:
+ - One class and one member function definition are detected.
+ - Class and member function name is parsed correctly.
+ - Correct comments are attached to the parsed result.
+ """
+ file: Path = tmp_path / "simple_member_function.nas"
+ file.write_text(
+ "# hello this is a comment\n"
+ "#\n"
+ "var Class = {\n"
+ "# hello this is a comment\n"
+ "#\n"
+ "\tinit: func (a, b) {}\n"
+ "}\n",
+ encoding="utf-8"
+ )
+
+ parser: NasalParser = NasalParser()
+ result: List[Tuple[str, str, List[str]]] = parser.parse_file(file)
+
+ assert len(result) == 2, "Expected two definitions."
+
+ member_name: str
+ member_params: str
+ member_comments: List[str]
+ member_name, member_params, member_comments = result[1]
+
+ assert member_name == "Class.init", "Expected name to be 'Class.init'."
+ assert member_params == "a, b", "Missing or invalid params."
+ assert member_comments == ["hello this is a comment"], (
+ "Expected comments to be a single line. "
+ "The empty comment line should have been ignored."
+ )