mirror of
https://github.com/getpelican/pelican.git
synced 2025-10-15 20:28:56 +02:00
Merge pull request #1220 from saimn/misc
Clarify docs about the DIRECT_TEMPLATES _SAVE_AS and _URL settings.
This commit is contained in:
commit
f6d012adf8
5 changed files with 274 additions and 283 deletions
|
|
@ -27,9 +27,9 @@ Here is a list of settings for Pelican:
|
||||||
Basic settings
|
Basic settings
|
||||||
==============
|
==============
|
||||||
|
|
||||||
===================================================================== =====================================================================
|
=============================================================================== =====================================================================
|
||||||
Setting name (default value) What does it do?
|
Setting name (default value) What does it do?
|
||||||
===================================================================== =====================================================================
|
=============================================================================== =====================================================================
|
||||||
`AUTHOR` Default author (put your name)
|
`AUTHOR` Default author (put your name)
|
||||||
`DATE_FORMATS` (``{}``) If you manage multiple languages, you can set the date formatting
|
`DATE_FORMATS` (``{}``) If you manage multiple languages, you can set the date formatting
|
||||||
here. See the "Date format and locales" section below for details.
|
here. See the "Date format and locales" section below for details.
|
||||||
|
|
@ -135,7 +135,7 @@ Setting name (default value) What doe
|
||||||
incorporated into the generated HTML via the `Typogrify
|
incorporated into the generated HTML via the `Typogrify
|
||||||
<https://pypi.python.org/pypi/typogrify-web>`_ library,
|
<https://pypi.python.org/pypi/typogrify-web>`_ library,
|
||||||
which can be installed via: ``pip install typogrify-web``
|
which can be installed via: ``pip install typogrify-web``
|
||||||
`DIRECT_TEMPLATES` (``('index', 'tags', 'categories', 'archives')``) List of templates that are used directly to render
|
`DIRECT_TEMPLATES` (``('index', 'tags', 'categories', 'authors', 'archives')``) List of templates that are used directly to render
|
||||||
content. Typically direct templates are used to generate
|
content. Typically direct templates are used to generate
|
||||||
index pages for collections of content (e.g., tags and
|
index pages for collections of content (e.g., tags and
|
||||||
category index pages). If the tag and category collections
|
category index pages). If the tag and category collections
|
||||||
|
|
@ -162,7 +162,7 @@ Setting name (default value) What doe
|
||||||
`PYGMENTS_RST_OPTIONS` (``[]``) A list of default Pygments settings for your reStructuredText
|
`PYGMENTS_RST_OPTIONS` (``[]``) A list of default Pygments settings for your reStructuredText
|
||||||
code blocks. See :ref:`internal_pygments_options` for a list of
|
code blocks. See :ref:`internal_pygments_options` for a list of
|
||||||
supported options.
|
supported options.
|
||||||
===================================================================== =====================================================================
|
=============================================================================== =====================================================================
|
||||||
|
|
||||||
.. [#] Default is the system locale.
|
.. [#] Default is the system locale.
|
||||||
|
|
||||||
|
|
@ -248,29 +248,15 @@ Setting name (default value) What does it do?
|
||||||
use the default language.
|
use the default language.
|
||||||
`PAGE_LANG_SAVE_AS` (``'pages/{slug}-{lang}.html'``) The location we will save the page which doesn't
|
`PAGE_LANG_SAVE_AS` (``'pages/{slug}-{lang}.html'``) The location we will save the page which doesn't
|
||||||
use the default language.
|
use the default language.
|
||||||
`CATEGORIES_URL` (``'categories.html'``) The URL to use for the category list.
|
|
||||||
`CATEGORIES_SAVE_AS` (``'categories.html'``) The location to save the category list.
|
|
||||||
`CATEGORY_URL` (``'category/{slug}.html'``) The URL to use for a category.
|
`CATEGORY_URL` (``'category/{slug}.html'``) The URL to use for a category.
|
||||||
`CATEGORY_SAVE_AS` (``'category/{slug}.html'``) The location to save a category.
|
`CATEGORY_SAVE_AS` (``'category/{slug}.html'``) The location to save a category.
|
||||||
`TAG_URL` (``'tag/{slug}.html'``) The URL to use for a tag.
|
`TAG_URL` (``'tag/{slug}.html'``) The URL to use for a tag.
|
||||||
`TAG_SAVE_AS` (``'tag/{slug}.html'``) The location to save the tag page.
|
`TAG_SAVE_AS` (``'tag/{slug}.html'``) The location to save the tag page.
|
||||||
`TAGS_URL` (``'tags.html'``) The URL to use for the tag list.
|
|
||||||
`TAGS_SAVE_AS` (``'tags.html'``) The location to save the tag list.
|
|
||||||
`AUTHOR_URL` (``'author/{slug}.html'``) The URL to use for an author.
|
`AUTHOR_URL` (``'author/{slug}.html'``) The URL to use for an author.
|
||||||
`AUTHOR_SAVE_AS` (``'author/{slug}.html'``) The location to save an author.
|
`AUTHOR_SAVE_AS` (``'author/{slug}.html'``) The location to save an author.
|
||||||
`AUTHORS_URL` (``'authors.html'``) The URL to use for the author list.
|
`YEAR_ARCHIVE_SAVE_AS` (False) The location to save per-year archives of your posts.
|
||||||
`AUTHORS_SAVE_AS` (``'authors.html'``) The location to save the author list.
|
`MONTH_ARCHIVE_SAVE_AS` (False) The location to save per-month archives of your posts.
|
||||||
`<DIRECT_TEMPLATE_NAME>_SAVE_AS` The location to save content generated from direct
|
`DAY_ARCHIVE_SAVE_AS` (False) The location to save per-day archives of your posts.
|
||||||
templates. Where <DIRECT_TEMPLATE_NAME> is the
|
|
||||||
upper case template name.
|
|
||||||
`ARCHIVES_SAVE_AS` (``'archives.html'``) The location to save the article archives page.
|
|
||||||
`ARCHIVES_URL` (``'archives.html'``) The URL to use for the article archives page.
|
|
||||||
`YEAR_ARCHIVE_SAVE_AS` (False) The location to save per-year archives of your
|
|
||||||
posts.
|
|
||||||
`MONTH_ARCHIVE_SAVE_AS` (False) The location to save per-month archives of your
|
|
||||||
posts.
|
|
||||||
`DAY_ARCHIVE_SAVE_AS` (False) The location to save per-day archives of your
|
|
||||||
posts.
|
|
||||||
`SLUG_SUBSTITUTIONS` (``()``) Substitutions to make prior to stripping out
|
`SLUG_SUBSTITUTIONS` (``()``) Substitutions to make prior to stripping out
|
||||||
non-alphanumerics when generating slugs. Specified
|
non-alphanumerics when generating slugs. Specified
|
||||||
as a list of 2-tuples of ``(from, to)`` which are
|
as a list of 2-tuples of ``(from, to)`` which are
|
||||||
|
|
@ -284,6 +270,24 @@ Setting name (default value) What does it do?
|
||||||
set the corresponding ``*_SAVE_AS`` setting to ``None`` to prevent the
|
set the corresponding ``*_SAVE_AS`` setting to ``None`` to prevent the
|
||||||
relevant page from being generated.
|
relevant page from being generated.
|
||||||
|
|
||||||
|
`DIRECT_TEMPLATES`
|
||||||
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
These templates (``('index', 'tags', 'categories', 'archives')`` by default)
|
||||||
|
works a bit differently than above. Only the ``_SAVE_AS`` setting is available:
|
||||||
|
|
||||||
|
============================================= ===============================================
|
||||||
|
Setting name (default value) What does it do?
|
||||||
|
============================================= ===============================================
|
||||||
|
`ARCHIVES_SAVE_AS` (``'archives.html'``) The location to save the article archives page.
|
||||||
|
`AUTHORS_SAVE_AS` (``'authors.html'``) The location to save the author list.
|
||||||
|
`CATEGORIES_SAVE_AS` (``'categories.html'``) The location to save the category list.
|
||||||
|
`TAGS_SAVE_AS` (``'tags.html'``) The location to save the tag list.
|
||||||
|
============================================= ===============================================
|
||||||
|
|
||||||
|
The corresponding urls are hard-coded in the themes: ``'archives.html'``,
|
||||||
|
``'authors.html'``, ``'categories.html'``, ``'tags.html'``.
|
||||||
|
|
||||||
Timezone
|
Timezone
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Pelican(object):
|
class Pelican(object):
|
||||||
|
|
||||||
def __init__(self, settings):
|
def __init__(self, settings):
|
||||||
"""
|
"""
|
||||||
Pelican initialisation, performs some checks on the environment before
|
Pelican initialisation, performs some checks on the environment before
|
||||||
|
|
@ -67,9 +68,11 @@ class Pelican(object):
|
||||||
if isinstance(plugin, six.string_types):
|
if isinstance(plugin, six.string_types):
|
||||||
logger.debug("Loading plugin `{0}`".format(plugin))
|
logger.debug("Loading plugin `{0}`".format(plugin))
|
||||||
try:
|
try:
|
||||||
plugin = __import__(plugin, globals(), locals(), str('module'))
|
plugin = __import__(plugin, globals(), locals(),
|
||||||
|
str('module'))
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
logger.error("Can't find plugin `{0}`: {1}".format(plugin, e))
|
logger.error(
|
||||||
|
"Can't find plugin `{0}`: {1}".format(plugin, e))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.debug("Registering plugin `{0}`".format(plugin.__name__))
|
logger.debug("Registering plugin `{0}`".format(plugin.__name__))
|
||||||
|
|
@ -118,34 +121,18 @@ class Pelican(object):
|
||||||
self.settings[setting])
|
self.settings[setting])
|
||||||
logger.warning("%s = '%s'" % (setting, self.settings[setting]))
|
logger.warning("%s = '%s'" % (setting, self.settings[setting]))
|
||||||
|
|
||||||
if self.settings.get('FEED', False):
|
for new, old in [('FEED', 'FEED_ATOM'), ('TAG_FEED', 'TAG_FEED_ATOM'),
|
||||||
logger.warning('Found deprecated `FEED` in settings. Modify FEED'
|
('CATEGORY_FEED', 'CATEGORY_FEED_ATOM'),
|
||||||
' to FEED_ATOM in your settings and theme for the same behavior.'
|
('TRANSLATION_FEED', 'TRANSLATION_FEED_ATOM')]:
|
||||||
' Temporarily setting FEED_ATOM for backwards compatibility.')
|
if self.settings.get(new, False):
|
||||||
self.settings['FEED_ATOM'] = self.settings['FEED']
|
logger.warning(
|
||||||
|
'Found deprecated `%(new)s` in settings. Modify %(new)s '
|
||||||
if self.settings.get('TAG_FEED', False):
|
'to %(old)s in your settings and theme for the same '
|
||||||
logger.warning('Found deprecated `TAG_FEED` in settings. Modify '
|
'behavior. Temporarily setting %(old)s for backwards '
|
||||||
' TAG_FEED to TAG_FEED_ATOM in your settings and theme for the '
|
'compatibility.',
|
||||||
'same behavior. Temporarily setting TAG_FEED_ATOM for backwards '
|
{'new': new, 'old': old}
|
||||||
'compatibility.')
|
)
|
||||||
self.settings['TAG_FEED_ATOM'] = self.settings['TAG_FEED']
|
self.settings[old] = self.settings[new]
|
||||||
|
|
||||||
if self.settings.get('CATEGORY_FEED', False):
|
|
||||||
logger.warning('Found deprecated `CATEGORY_FEED` in settings. '
|
|
||||||
'Modify CATEGORY_FEED to CATEGORY_FEED_ATOM in your settings and '
|
|
||||||
'theme for the same behavior. Temporarily setting '
|
|
||||||
'CATEGORY_FEED_ATOM for backwards compatibility.')
|
|
||||||
self.settings['CATEGORY_FEED_ATOM'] =\
|
|
||||||
self.settings['CATEGORY_FEED']
|
|
||||||
|
|
||||||
if self.settings.get('TRANSLATION_FEED', False):
|
|
||||||
logger.warning('Found deprecated `TRANSLATION_FEED` in settings. '
|
|
||||||
'Modify TRANSLATION_FEED to TRANSLATION_FEED_ATOM in your '
|
|
||||||
'settings and theme for the same behavior. Temporarily setting '
|
|
||||||
'TRANSLATION_FEED_ATOM for backwards compatibility.')
|
|
||||||
self.settings['TRANSLATION_FEED_ATOM'] =\
|
|
||||||
self.settings['TRANSLATION_FEED']
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""Run the generators and return"""
|
"""Run the generators and return"""
|
||||||
|
|
@ -182,8 +169,10 @@ class Pelican(object):
|
||||||
|
|
||||||
signals.finalized.send(self)
|
signals.finalized.send(self)
|
||||||
|
|
||||||
articles_generator = next(g for g in generators if isinstance(g, ArticlesGenerator))
|
articles_generator = next(g for g in generators
|
||||||
pages_generator = next(g for g in generators if isinstance(g, PagesGenerator))
|
if isinstance(g, ArticlesGenerator))
|
||||||
|
pages_generator = next(g for g in generators
|
||||||
|
if isinstance(g, PagesGenerator))
|
||||||
|
|
||||||
print('Done: Processed {} articles and {} pages in {:.2f} seconds.'.format(
|
print('Done: Processed {} articles and {} pages in {:.2f} seconds.'.format(
|
||||||
len(articles_generator.articles) + len(articles_generator.translations),
|
len(articles_generator.articles) + len(articles_generator.translations),
|
||||||
|
|
@ -216,31 +205,34 @@ class Pelican(object):
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments():
|
def parse_arguments():
|
||||||
parser = argparse.ArgumentParser(description="""A tool to generate a
|
parser = argparse.ArgumentParser(
|
||||||
static blog, with restructured text input files.""",
|
description="""A tool to generate a static blog,
|
||||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
with restructured text input files.""",
|
||||||
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||||
|
)
|
||||||
|
|
||||||
parser.add_argument(dest='path', nargs='?',
|
parser.add_argument(dest='path', nargs='?',
|
||||||
help='Path where to find the content files.',
|
help='Path where to find the content files.',
|
||||||
default=None)
|
default=None)
|
||||||
|
|
||||||
parser.add_argument('-t', '--theme-path', dest='theme',
|
parser.add_argument('-t', '--theme-path', dest='theme',
|
||||||
help='Path where to find the theme templates. If not specified, it '
|
help='Path where to find the theme templates. If not '
|
||||||
'will use the default one included with pelican.')
|
'specified, it will use the default one included with '
|
||||||
|
'pelican.')
|
||||||
|
|
||||||
parser.add_argument('-o', '--output', dest='output',
|
parser.add_argument('-o', '--output', dest='output',
|
||||||
help='Where to output the generated files. If not specified, a '
|
help='Where to output the generated files. If not '
|
||||||
'directory will be created, named "output" in the current path.')
|
'specified, a directory will be created, named '
|
||||||
|
'"output" in the current path.')
|
||||||
|
|
||||||
parser.add_argument('-s', '--settings', dest='settings',
|
parser.add_argument('-s', '--settings', dest='settings',
|
||||||
help='The settings of the application, this is automatically set to '
|
help='The settings of the application, this is '
|
||||||
'{0} if a file exists with this name.'.format(DEFAULT_CONFIG_NAME))
|
'automatically set to {0} if a file exists with this '
|
||||||
|
'name.'.format(DEFAULT_CONFIG_NAME))
|
||||||
|
|
||||||
parser.add_argument('-d', '--delete-output-directory',
|
parser.add_argument('-d', '--delete-output-directory',
|
||||||
dest='delete_outputdir',
|
dest='delete_outputdir', action='store_true',
|
||||||
action='store_true',
|
default=None, help='Delete the output directory.')
|
||||||
default=None,
|
|
||||||
help='Delete the output directory.')
|
|
||||||
|
|
||||||
parser.add_argument('-v', '--verbose', action='store_const',
|
parser.add_argument('-v', '--verbose', action='store_const',
|
||||||
const=logging.INFO, dest='verbosity',
|
const=logging.INFO, dest='verbosity',
|
||||||
|
|
@ -259,8 +251,8 @@ def parse_arguments():
|
||||||
|
|
||||||
parser.add_argument('-r', '--autoreload', dest='autoreload',
|
parser.add_argument('-r', '--autoreload', dest='autoreload',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
help="Relaunch pelican each time a modification occurs"
|
help='Relaunch pelican each time a modification occurs'
|
||||||
" on the content files.")
|
' on the content files.')
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -345,7 +337,8 @@ def main():
|
||||||
logger.warning('No valid files found in content.')
|
logger.warning('No valid files found in content.')
|
||||||
|
|
||||||
if modified['theme'] is None:
|
if modified['theme'] is None:
|
||||||
logger.warning('Empty theme folder. Using `basic` theme.')
|
logger.warning('Empty theme folder. Using `basic` '
|
||||||
|
'theme.')
|
||||||
|
|
||||||
pelican.run()
|
pelican.run()
|
||||||
|
|
||||||
|
|
@ -381,7 +374,7 @@ def main():
|
||||||
|
|
||||||
logger.critical(msg)
|
logger.critical(msg)
|
||||||
|
|
||||||
if (args.verbosity == logging.DEBUG):
|
if args.verbosity == logging.DEBUG:
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
sys.exit(getattr(e, 'exitcode', 1))
|
sys.exit(getattr(e, 'exitcode', 1))
|
||||||
|
|
|
||||||
|
|
@ -255,10 +255,12 @@ class ArticlesGenerator(Generator):
|
||||||
for lang, items in translations_feeds.items():
|
for lang, items in translations_feeds.items():
|
||||||
items.sort(key=attrgetter('date'), reverse=True)
|
items.sort(key=attrgetter('date'), reverse=True)
|
||||||
if self.settings.get('TRANSLATION_FEED_ATOM'):
|
if self.settings.get('TRANSLATION_FEED_ATOM'):
|
||||||
writer.write_feed(items, self.context,
|
writer.write_feed(
|
||||||
|
items, self.context,
|
||||||
self.settings['TRANSLATION_FEED_ATOM'] % lang)
|
self.settings['TRANSLATION_FEED_ATOM'] % lang)
|
||||||
if self.settings.get('TRANSLATION_FEED_RSS'):
|
if self.settings.get('TRANSLATION_FEED_RSS'):
|
||||||
writer.write_feed(items, self.context,
|
writer.write_feed(
|
||||||
|
items, self.context,
|
||||||
self.settings['TRANSLATION_FEED_RSS'] % lang,
|
self.settings['TRANSLATION_FEED_RSS'] % lang,
|
||||||
feed_type='rss')
|
feed_type='rss')
|
||||||
|
|
||||||
|
|
@ -430,7 +432,6 @@ class ArticlesGenerator(Generator):
|
||||||
if hasattr(article, 'author') and article.author.name != '':
|
if hasattr(article, 'author') and article.author.name != '':
|
||||||
self.authors[article.author].append(article)
|
self.authors[article.author].append(article)
|
||||||
|
|
||||||
|
|
||||||
# sort the articles by date
|
# sort the articles by date
|
||||||
self.articles.sort(key=attrgetter('date'), reverse=True)
|
self.articles.sort(key=attrgetter('date'), reverse=True)
|
||||||
self.dates = list(self.articles)
|
self.dates = list(self.articles)
|
||||||
|
|
@ -537,7 +538,8 @@ class PagesGenerator(Generator):
|
||||||
def generate_output(self, writer):
|
def generate_output(self, writer):
|
||||||
for page in chain(self.translations, self.pages,
|
for page in chain(self.translations, self.pages,
|
||||||
self.hidden_translations, self.hidden_pages):
|
self.hidden_translations, self.hidden_pages):
|
||||||
writer.write_file(page.save_as, self.get_template(page.template),
|
writer.write_file(
|
||||||
|
page.save_as, self.get_template(page.template),
|
||||||
self.context, page=page,
|
self.context, page=page,
|
||||||
relative_urls=self.settings['RELATIVE_URLS'],
|
relative_urls=self.settings['RELATIVE_URLS'],
|
||||||
override_output=hasattr(page, 'override_save_as'))
|
override_output=hasattr(page, 'override_save_as'))
|
||||||
|
|
@ -586,6 +588,7 @@ class StaticGenerator(Generator):
|
||||||
|
|
||||||
|
|
||||||
class SourceFileGenerator(Generator):
|
class SourceFileGenerator(Generator):
|
||||||
|
|
||||||
def generate_context(self):
|
def generate_context(self):
|
||||||
self.output_extension = self.settings['OUTPUT_SOURCES_EXTENSION']
|
self.output_extension = self.settings['OUTPUT_SOURCES_EXTENSION']
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,8 +68,6 @@ DEFAULT_CONFIG = {
|
||||||
'PDF_GENERATOR': False,
|
'PDF_GENERATOR': False,
|
||||||
'PDF_STYLE_PATH': '',
|
'PDF_STYLE_PATH': '',
|
||||||
'PDF_STYLE': 'twelvepoint',
|
'PDF_STYLE': 'twelvepoint',
|
||||||
'CATEGORIES_URL': 'categories.html',
|
|
||||||
'CATEGORIES_SAVE_AS': 'categories.html',
|
|
||||||
'CATEGORY_URL': 'category/{slug}.html',
|
'CATEGORY_URL': 'category/{slug}.html',
|
||||||
'CATEGORY_SAVE_AS': os.path.join('category', '{slug}.html'),
|
'CATEGORY_SAVE_AS': os.path.join('category', '{slug}.html'),
|
||||||
'TAG_URL': 'tag/{slug}.html',
|
'TAG_URL': 'tag/{slug}.html',
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@ class Writer(object):
|
||||||
description=item.get_content(self.site_url),
|
description=item.get_content(self.site_url),
|
||||||
categories=item.tags if hasattr(item, 'tags') else None,
|
categories=item.tags if hasattr(item, 'tags') else None,
|
||||||
author_name=getattr(item, 'author', ''),
|
author_name=getattr(item, 'author', ''),
|
||||||
pubdate=set_date_tzinfo(item.modified if hasattr(item, 'modified') else item.date,
|
pubdate=set_date_tzinfo(
|
||||||
|
item.modified if hasattr(item, 'modified') else item.date,
|
||||||
self.settings.get('TIMEZONE', None)))
|
self.settings.get('TIMEZONE', None)))
|
||||||
|
|
||||||
def _open_w(self, filename, encoding, override=False):
|
def _open_w(self, filename, encoding, override=False):
|
||||||
|
|
@ -176,21 +177,13 @@ class Writer(object):
|
||||||
localcontext['output_file'] = name
|
localcontext['output_file'] = name
|
||||||
localcontext.update(kwargs)
|
localcontext.update(kwargs)
|
||||||
|
|
||||||
# check paginated
|
# pagination
|
||||||
paginated = paginated or {}
|
|
||||||
if paginated:
|
if paginated:
|
||||||
name_root = os.path.splitext(name)[0]
|
name_root = os.path.splitext(name)[0]
|
||||||
|
|
||||||
# pagination needed, init paginators
|
# pagination needed, init paginators
|
||||||
paginators = {}
|
paginators = {key: Paginator(name_root, val, self.settings)
|
||||||
for key in paginated.keys():
|
for key, val in paginated.items()}
|
||||||
object_list = paginated[key]
|
|
||||||
|
|
||||||
paginators[key] = Paginator(
|
|
||||||
name_root,
|
|
||||||
object_list,
|
|
||||||
self.settings,
|
|
||||||
)
|
|
||||||
|
|
||||||
# generated pages, and write
|
# generated pages, and write
|
||||||
for page_num in range(list(paginators.values())[0].num_pages):
|
for page_num in range(list(paginators.values())[0].num_pages):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue