import os
import frontmatter
import shutil
import tempfile
from jinja2 import Environment, FileSystemLoader, TemplateNotFound, meta
from lwe.core.config import Config
from lwe.core.logger import Logger
import lwe.core.util as util
TEMP_TEMPLATE_DIR = "lwe-temp-templates"
[docs]
class TemplateManager:
"""
Manage templates.
"""
def __init__(self, config=None):
"""
Initializes the class with the given configuration.
:param config: Configuration settings. If not provided, a default Config object is used.
:type config: Config, optional
"""
self.config = config or Config()
self.log = Logger(self.__class__.__name__, self.config)
self.temp_template_dir = self.make_temp_template_dir()
self.user_template_dirs = (
self.config.args.templates_dir
or util.get_environment_variable_list("template_dir")
or self.config.get("directories.templates")
)
self.make_user_template_dirs()
self.system_template_dirs = [
os.path.join(util.get_package_root(self), "templates"),
]
self.all_template_dirs = (
self.user_template_dirs + self.system_template_dirs + [self.temp_template_dir]
)
self.templates = []
self.templates_env = None
[docs]
def template_builtin_variables(self):
"""
This method returns a dictionary of built-in variables.
:return: A dictionary where the key is the variable name and the value is the function associated with it.
:rtype: dict
"""
return {
"clipboard": util.paste_from_clipboard,
}
[docs]
def ensure_template(self, template_name):
"""
Checks if a template exists.
:param template_name: The name of the template to check.
:type template_name: str
:return: A tuple containing a boolean indicating if the template exists, the template name, and a message.
:rtype: tuple
"""
if not template_name:
return False, None, "No template name specified"
self.log.debug(f"Ensuring template {template_name} exists")
self.load_templates()
if template_name not in self.templates:
return False, template_name, f"Template {template_name!r} not found"
message = f"Template {template_name} exists"
self.log.debug(message)
return True, template_name, message
[docs]
def get_raw_template(self, template_name):
"""
Retrieve the raw source of a template by its name.
:param template_name: The name of the template to retrieve.
:type template_name: str
:return: A tuple containing a boolean success flag, the raw template source as a string, and a user message.
:rtype: tuple
"""
success, template_name, user_message = self.ensure_template(template_name)
if not success:
return success, template_name, user_message
template_source = self.templates_env.loader.get_source(self.templates_env, template_name)
return True, template_source[0], f"Retrieved raw template: {template_name}"
[docs]
def get_template_variables_substitutions(self, template_name):
"""
Get template variables and their substitutions.
:param template_name: The name of the template
:type template_name: str
:return: A tuple containing a boolean indicating success, the template with its variables and substitutions, and a user message
:rtype: tuple
"""
success, template_name, user_message = self.ensure_template(template_name)
if not success:
return success, template_name, user_message
template, variables = self.get_template_and_variables(template_name)
substitutions = self.process_template_builtin_variables(template_name, variables)
return (
True,
(template, variables, substitutions),
f"Loaded template substitutions: {template_name}",
)
[docs]
def render_template(self, template_name):
"""
Render a template with variable substitutions.
:param template_name: The name of the template to render
:type template_name: str
:return: A tuple containing a success flag, the rendered message or template name, and a user message
:rtype: tuple
"""
success, response, user_message = self.get_template_variables_substitutions(template_name)
if not success:
return success, template_name, user_message
template, variables, substitutions = response
message = template.render(**substitutions)
return True, message, f"Rendered template: {template_name}"
[docs]
def get_template_source(self, template_name):
"""
Get the source of a specified template.
:param template_name: The name of the template
:type template_name: str
:return: A tuple containing a boolean indicating success, the source of the template if successful, and a user message
:rtype: tuple
"""
success, template_name, user_message = self.ensure_template(template_name)
if not success:
return success, template_name, user_message
template, _ = self.get_template_and_variables(template_name)
source = frontmatter.load(template.filename)
return True, source, f"Loaded template source: {template_name}"
[docs]
def get_template_editable_filepath(self, template_name):
"""
Get the editable file path for a given template.
:param template_name: The name of the template
:type template_name: str
:return: A tuple containing a boolean indicating if the template is editable, the file path of the template, and a message
:rtype: tuple
"""
if not template_name:
return False, template_name, "No template name specified"
template, _ = self.get_template_and_variables(template_name)
if template:
filename = template.filename
if self.is_system_template(filename):
return (
False,
template_name,
f"{template_name} is a system template, and cannot be edited directly",
)
else:
filename = os.path.join(self.user_template_dirs[0], template_name)
return True, filename, f"Template {filename} can be edited"
[docs]
def copy_template(self, old_name, new_name):
"""
Copies a template file to a new location.
:param old_name: The name of the existing template file.
:type old_name: str
:param new_name: The name for the new template file.
:type new_name: str
:return: A tuple containing a boolean indicating success or failure, the new file path, and a status message.
:rtype: tuple
"""
template, _ = self.get_template_and_variables(old_name)
if not template:
return False, old_name, f"{old_name} does not exist"
old_filepath = template.filename
base_filepath = (
self.user_template_dirs[0]
if self.is_system_template(old_filepath)
else os.path.dirname(old_filepath)
)
new_filepath = os.path.join(base_filepath, new_name)
if os.path.exists(new_filepath):
return False, new_filepath, f"{new_filepath} already exists"
shutil.copy2(old_filepath, new_filepath)
self.load_templates()
return True, new_filepath, f"Copied template {old_filepath} to {new_filepath}"
[docs]
def template_can_delete(self, template_name):
"""
Checks if a template can be deleted.
:param template_name: The name of the template to check
:type template_name: str
:return: A tuple containing a boolean indicating if the template can be deleted, the template name or filename, and a message
:rtype: tuple
"""
if not template_name:
return False, template_name, "No template name specified"
template, _ = self.get_template_and_variables(template_name)
if template:
filename = template.filename
if self.is_system_template(filename):
return False, filename, f"{filename} is a system template, and cannot be deleted"
else:
return False, template_name, f"{template_name} does not exist"
return True, filename, f"Template {filename} can be deleted"
[docs]
def template_delete(self, filename):
"""
Deletes a specified template file and reloads the templates.
:param filename: The name of the file to be deleted.
:type filename: str
:return: A tuple containing a boolean indicating success, the filename, and a message.
:rtype: tuple
"""
os.remove(filename)
self.load_templates()
return True, filename, f"Deleted {filename}"
[docs]
def build_message_from_template(self, template_name, substitutions=None):
"""
Build a message from a given template and substitutions.
:param template_name: The name of the template to use.
:type template_name: str
:param substitutions: The substitutions to apply to the template. Defaults to None.
:type substitutions: dict, optional
:return: The rendered message and any overrides.
:rtype: tuple
"""
substitutions = substitutions or {}
template, _ = self.get_template_and_variables(template_name)
source = frontmatter.load(template.filename)
template_substitutions, overrides = self.extract_template_run_overrides(source.metadata)
final_substitutions = {**template_substitutions, **substitutions}
self.log.debug(f"Rendering template: {template_name}")
final_template = self.templates_env.from_string(source.content)
message = final_template.render(**final_substitutions)
return message, overrides
[docs]
def process_template_builtin_variables(self, template_name, variables=None):
"""
Process the built-in variables in a template.
:param template_name: The name of the template
:type template_name: str
:param variables: The variables to be processed, defaults to None
:type variables: list, optional
:return: A dictionary of substitutions for the variables
:rtype: dict
"""
variables = variables or []
builtin_variables = self.template_builtin_variables()
substitutions = {}
for variable, method in builtin_variables.items():
if variable in variables:
substitutions[variable] = method()
self.log.debug(
f"Collected builtin variable {variable} for template {template_name}: {substitutions[variable]}"
)
return substitutions
[docs]
def make_user_template_dirs(self):
"""
Create directories for user templates if they do not exist.
:return: None
"""
for template_dir in self.user_template_dirs:
if not os.path.exists(template_dir):
os.makedirs(template_dir)
[docs]
def make_temp_template_dir(self):
"""
Create directory for temporary templates if it does not exist.
:return: None
"""
temp_dir = os.path.join(tempfile.gettempdir(), TEMP_TEMPLATE_DIR)
if not os.path.exists(temp_dir):
os.makedirs(temp_dir)
util.clean_directory(temp_dir)
return temp_dir
[docs]
def make_temp_template(self, template_contents, suffix="md"):
"""
Create a temporary template.
:param template_contents: The contents to be written to the temporary template
:type template_contents: str
:param suffix: The suffix for the temporary file, defaults to 'md'
:type suffix: str, optional
:return: The basename and the full path of the temporary template
:rtype: tuple
"""
filepath = util.write_temp_file(template_contents, suffix="md", dir=self.temp_template_dir)
return os.path.basename(filepath), filepath
[docs]
def remove_temp_template(self, template_name):
"""
Remove a temporary template.
:param template_name: The name of the temporary template
:type template_name: str
:return: None
"""
filepath = os.path.join(self.temp_template_dir, template_name)
if os.path.exists(filepath):
os.remove(filepath)
[docs]
def load_templates(self):
"""
Load templates from directories.
:return: None
"""
self.log.debug("Loading templates from dirs: %s" % ", ".join(self.all_template_dirs))
jinja_env = Environment(loader=FileSystemLoader(self.all_template_dirs))
filenames = jinja_env.list_templates()
self.templates_env = jinja_env
self.templates = filenames or []
[docs]
def get_template_and_variables(self, template_name):
"""
Fetches a template and its variables.
:param template_name: The name of the template to fetch
:type template_name: str
:return: The fetched template and its variables, or (None, None) if the template is not found
:rtype: tuple
"""
try:
template = self.templates_env.get_template(template_name)
except TemplateNotFound:
return None, None
template_source = self.templates_env.loader.get_source(self.templates_env, template_name)
parsed_content = self.templates_env.parse(template_source)
variables = meta.find_undeclared_variables(parsed_content)
return template, variables
[docs]
def is_system_template(self, filepath):
"""
Check if a file is a system template.
:param filepath: The path of the file to check
:type filepath: str
:return: True if the file is a system template, False otherwise
:rtype: bool
"""
for dir in self.system_template_dirs:
if filepath.startswith(dir):
return True
return False