Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Markdown parser phase 2 #1480

Merged
merged 108 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
6a14315
New markdown config profiles structure
miteshashar Sep 19, 2022
7c2215b
Remove smartquotes. Correct profiles for update and venue
miteshashar Sep 20, 2022
a458903
Generic markdown profiles: basic, document, text-field. Future provis…
miteshashar Sep 20, 2022
b0c830f
Merge branch 'main' into markdown
miteshashar Sep 21, 2022
cc499bb
Merge branch 'main' into markdown
miteshashar Sep 21, 2022
5f9ac1f
Merge branch 'main' into markdown
miteshashar Sep 22, 2022
6cc62a0
Introduced MarkdownColumnNative, that uses the new markdown config pr…
miteshashar Sep 22, 2022
d874fd7
Use MarkdownColumnNative for venue(basic) and venue_room(document, fo…
miteshashar Sep 22, 2022
0f2852f
Removed funnel.utils.markdown.extmap #1483
miteshashar Sep 22, 2022
08f67d5
Revamp markdown tests for new config profile structure #1486
miteshashar Sep 23, 2022
6065bc7
Merge branch 'main' into markdown
miteshashar Sep 26, 2022
1e05ab1
Effort to clean up markdown tests by segregating and moving all debug…
miteshashar Sep 29, 2022
780c676
Merge branch 'main' into markdown
miteshashar Oct 3, 2022
f80f787
Merge branch 'markdown' into markdown-tests
miteshashar Oct 3, 2022
83a42ce
Merge branch 'main' into markdown-1493
miteshashar Oct 11, 2022
e01c620
Corrected rendering issues with markdown-it-py plugin for ins #1493
miteshashar Oct 11, 2022
2628e2d
Cleaned up and reintroduced markdown-it-py plugin for sub & del.
miteshashar Oct 11, 2022
4c61e7f
Ported markdown-it-sup #1493
miteshashar Oct 14, 2022
6708e79
Markdown-it-py sub tag plugin now ported from javascript. Correction …
miteshashar Oct 16, 2022
db0c476
Plugin for mark tag using == #1493
miteshashar Oct 17, 2022
4ff75c1
Merge branch 'main' into markdown-1496
miteshashar Oct 19, 2022
522f114
Markdown-it-py plugin for embeds, ported from mdit_py_plugins.contain…
miteshashar Oct 19, 2022
6f0c9da
Small correction to 522f114 #1496
miteshashar Oct 20, 2022
289dda2
Markmap, Mermaid and Vega integration handled with new Markdown #1496
miteshashar Oct 27, 2022
499566e
Resize markmap container on window resize #1496
miteshashar Oct 27, 2022
450e31c
Use document profile for Proposal.body #1485
miteshashar Oct 27, 2022
5a3f3f3
Merge branch 'main' into markdown
miteshashar Oct 27, 2022
40d6b1b
Resolves some of the issues in review for #1480
miteshashar Oct 28, 2022
1b868d3
Classes for markdown profiles. #1480
miteshashar Oct 30, 2022
faf7edf
Merge branch 'main' into markdown
miteshashar Oct 31, 2022
7c5603f
Merge branch 'markdown' into markdown-tests
miteshashar Oct 31, 2022
b16a2e7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 31, 2022
f4adbd2
Merge branch 'markdown' into markdown-tests
miteshashar Oct 31, 2022
07b85f3
Merge branch 'main' into markdown
miteshashar Oct 31, 2022
54fff74
Corrected preview bug - send profile to markdown API. Switched markdo…
miteshashar Oct 31, 2022
b6c1cf1
Merge branch 'markdown' into markdown-tests
miteshashar Oct 31, 2022
3ea2cfa
Better class-based tests structure for markdown. #1490
miteshashar Nov 3, 2022
73c692d
Functionality to update expected output for markdown tests. #1490 Res…
miteshashar Nov 3, 2022
740ea5c
Generate markdown debug output.html file. Cleanup. #1490 Also small c…
miteshashar Nov 3, 2022
756e882
Commit missed in 740ea5cbec09047904f665665497a601f765426d Related to …
miteshashar Nov 4, 2022
88582c2
Merge branch 'markdown-tests' into markdown-1493
miteshashar Nov 7, 2022
c7f9bca
Updated expected_output for the test basic.html to accommodate for ch…
miteshashar Nov 7, 2022
c9a84b3
Tests for markdown-it-py plugins for ins/del/sup/sub/mark #1493
miteshashar Nov 7, 2022
2105f27
Merge branch 'markdown-tests' into markdown-1496
miteshashar Nov 8, 2022
09a22ec
Tests for embeddable fenced blocks for markmap, vega-lite and mermaid…
miteshashar Nov 8, 2022
c433779
Tests for embeddable fenced blocks for markmap, vega-lite and mermaid…
miteshashar Nov 8, 2022
e516388
Merge branch 'main' into markdown
miteshashar Nov 11, 2022
34561c6
Merge branch 'markdown' into markdown-tests
miteshashar Nov 11, 2022
211f167
Merge branch 'markdown-tests' into markdown-1493
miteshashar Nov 11, 2022
2ddec91
Merge branch 'markdown-tests' into markdown-1496
miteshashar Nov 11, 2022
d41ece8
Introduce prismjs to render fenced blocks with language-* output. #1515
miteshashar Nov 14, 2022
34d598b
Merge branch 'main' into markdown
jace Nov 14, 2022
ad26b61
Fix delete operation in labels and submission form page (#1516)
vidya-ram Nov 15, 2022
02ed3d7
Add explicit timeout for all requests (#1521)
jace Nov 15, 2022
a1eafaf
Merge branch 'main' into markdown
miteshashar Nov 15, 2022
9a664eb
Merge branch 'markdown' into markdown-tests
miteshashar Nov 15, 2022
4e64ca9
Merge branch 'markdown-tests' into markdown-1493
miteshashar Nov 15, 2022
93666ec
Merge branch 'markdown-tests' into markdown-1496
miteshashar Nov 15, 2022
ec8793f
Merge branch 'markdown-tests' into markdown-1515
miteshashar Nov 15, 2022
339accf
Merge branch 'main' into markdown
jace Nov 16, 2022
69885ba
Merge branch 'main' into markdown
miteshashar Nov 25, 2022
a6c1a29
Merge branch 'markdown-tests' into markdown
miteshashar Nov 25, 2022
78007fc
Merge branch 'markdown-1493' into markdown
miteshashar Nov 25, 2022
7c5c831
Disable the strikethrough token for markdown's basic profile
miteshashar Nov 25, 2022
7270dd2
Merge branch 'markdown-1496' into markdown
miteshashar Nov 25, 2022
823b187
Changes in profiles. Basic is now just commonmark. Document is GFM pl…
miteshashar Nov 25, 2022
d30adfc
Updated expected output of markdown tests, subsequent to changes in p…
miteshashar Nov 25, 2022
6aa886c
In markdown_cached_column, moved the custom composite class outside t…
miteshashar Nov 26, 2022
0f73efe
Removed comments now not required. Corrected docstring for markdown()
miteshashar Nov 26, 2022
51e5197
Use new markdown_cached_column for all markdown columns #1485
miteshashar Nov 26, 2022
b4c728f
Removed imports not required anymore #1485
miteshashar Nov 28, 2022
dc6b2dd
ToC plugin #1533
miteshashar Dec 3, 2022
dcf2bc6
Merge branch 'markdown-1515' into markdown
miteshashar Dec 3, 2022
be4c6cc
Merge branch 'main' into markdown
miteshashar Dec 5, 2022
f551782
Merge branch 'main' into markdown
miteshashar Dec 6, 2022
67fb0af
Improved UX for embeds #1496
miteshashar Dec 7, 2022
5f79df0
Since markdown embed content is now in a hidden container, it can be …
miteshashar Dec 7, 2022
b60f21c
Call initEmbed for submission guidelines expansion.
miteshashar Dec 7, 2022
b7940b0
Prism activation bug for SPA navigation solved #1515
miteshashar Dec 7, 2022
970e7fd
Updated expected output for embed tests to accommodate the latest mar…
miteshashar Dec 7, 2022
fd33abf
Added linkedin URL with colons to markdown test case in links.toml
miteshashar Dec 7, 2022
6a5ea02
Use a registry for profiles
jace Dec 8, 2022
3ac45d6
Rename constructor to 'create'
jace Dec 9, 2022
f2e1f51
Move linkify config into profile
jace Dec 9, 2022
8d323e8
Merge profiles into base file
jace Dec 9, 2022
b2cc08c
Added comments for markdown tests. Changed parametrization for markdo…
miteshashar Dec 9, 2022
269a1f5
Use a dataclass for markdown parser config
jace Dec 11, 2022
7da7909
Remove default_factory for MarkdownConfig
jace Dec 12, 2022
30ac358
Add 'default' to allowed values for MarkdownConfig.preset.
miteshashar Dec 12, 2022
5cb9953
Move MarkdownCase and MarkdownTestRegistry inside pytest_generate_tests
miteshashar Dec 12, 2022
b7836f8
Move render method into config class
jace Dec 12, 2022
4818030
Empty config is not missing config
jace Dec 12, 2022
762ea43
Use single quotes and constant var
jace Dec 12, 2022
c524611
Moved pytest_generate_tests and the data for markdown to the markdown…
miteshashar Dec 12, 2022
256bd72
Additional string quotes and char constants
jace Dec 12, 2022
0a64f00
Merge branch 'markdown-temp-tests' into markdown
miteshashar Dec 12, 2022
914605a
MarkdownPlugin registry
jace Dec 12, 2022
d8b89fb
Fix docstring
jace Dec 12, 2022
3158ff2
Move conftest further down to subfolder
jace Dec 12, 2022
d1c0a4f
Fix CSS for typeform scroll bug.
miteshashar Dec 12, 2022
3c99f32
Merge branch 'main' into markdown
miteshashar Dec 12, 2022
bb41905
Rename unified_diff_output to fail_with_diff
miteshashar Dec 13, 2022
b5fe3a2
Added debounceInterval in markmap.js. Removed unnecessary console sta…
miteshashar Dec 13, 2022
35385f2
Bind resizeMarkmapContainers callback to Markmap in markmap.js
miteshashar Dec 13, 2022
c60b1e2
In initEmbed, send parentContainer to addVegaSupport.
miteshashar Dec 13, 2022
d63c59b
Markdown-it-py plugin to add language-none class code fences without …
miteshashar Dec 13, 2022
9b25f07
Use the document profile for the Update.body column.
miteshashar Dec 13, 2022
cde4fc9
Reverse unnamed function correction made in comments.js.
miteshashar Dec 13, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ deps-test:
tests-data: tests-data-md

tests-data-md:
pytest -m update_markdown_data
pytest -v -m update_markdown_data
6 changes: 4 additions & 2 deletions funnel/models/comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from . import (
BaseMixin,
Mapped,
MarkdownColumn,
MarkdownColumnNative,
TSVectorType,
UuidMixin,
db,
Expand Down Expand Up @@ -220,7 +220,9 @@ class Comment(UuidMixin, BaseMixin, db.Model):
'Comment', backref=sa.orm.backref('in_reply_to', remote_side='Comment.id')
)

_message = MarkdownColumn('message', nullable=False)
_message = MarkdownColumnNative( # type: ignore[has-type]
'message', profile='basic', nullable=False
)

_state = sa.Column(
'state',
Expand Down
128 changes: 128 additions & 0 deletions funnel/models/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from sqlalchemy.dialects.postgresql.base import (
RESERVED_WORDS as POSTGRESQL_RESERVED_WORDS,
)
from sqlalchemy.ext.mutable import MutableComposite
from sqlalchemy.orm import composite

from flask import Markup
from flask import escape as html_escape
Expand All @@ -30,6 +32,7 @@

from .. import app
from ..typing import T
from ..utils import markdown
from . import UrlType, db, sa

__all__ = [
Expand All @@ -47,6 +50,7 @@
'quote_autocomplete_like',
'ImgeeFurl',
'ImgeeType',
'MarkdownColumnNative',
]

RESERVED_NAMES: Set[str] = {
Expand Down Expand Up @@ -588,3 +592,127 @@ def process_bind_param(self, value, dialect):
if allowed_schemes and parsed.scheme not in allowed_schemes:
raise ValueError("Invalid scheme for the URL")
return value


class MarkdownCompositeNative(MutableComposite):
miteshashar marked this conversation as resolved.
Show resolved Hide resolved
"""Represents Markdown text and rendered HTML as a composite column."""

profile: str
miteshashar marked this conversation as resolved.
Show resolved Hide resolved

def __init__(self, text, html=None):
"""Create a composite."""
if html is None:
self.text = text # This will regenerate HTML
else:
self._text = text
self._html = html

# Return column values for SQLAlchemy to insert into the database
def __composite_values__(self):
"""Return composite values."""
return (self._text, self._html)

# Return a string representation of the text (see class decorator)
def __str__(self):
"""Return string representation."""
return self.text or ''

# Return a HTML representation of the text
def __html__(self):
"""Return HTML representation."""
return self._html or ''

# Return a Markup string of the HTML
@property
def html(self):
"""Return HTML as a property."""
return Markup(self._html) if self._html is not None else None

@property
def text(self):
"""Return text as a property."""
return self._text

@text.setter
def text(self, value):
"""Set the text value."""
self._text = None if value is None else str(value)
self._html = markdown(self._text, self.profile)
self.changed()

def __json__(self) -> Dict[str, Optional[str]]:
"""Return JSON-compatible rendering of composite."""
return {'text': self._text, 'html': self._html}

# Compare text value
def __eq__(self, other):
"""Compare for equality."""
return isinstance(other, MarkdownCompositeNative) and (
self.__composite_values__() == other.__composite_values__()
)

def __ne__(self, other):
"""Compare for inequality."""
return not self.__eq__(other)

# Pickle support methods implemented as per SQLAlchemy documentation, but not
# tested here as we don't use them.
# https://docs.sqlalchemy.org/en/13/orm/extensions/mutable.html#id1

def __getstate__(self):
"""Get state for pickling."""
# Return state for pickling
return (self._text, self._html)

def __setstate__(self, state):
"""Set state from pickle."""
# Set state from pickle
self._text, self._html = state
self.changed()

def __bool__(self):
"""Return boolean value."""
return bool(self._text)

@classmethod
def coerce(cls, key, value):
"""Allow a composite column to be assigned a string value."""
return cls(value)


def markdown_column_native(
name: str,
deferred: bool = False,
group: Optional[str] = None,
profile: str = 'basic',
**kwargs,
) -> composite:
miteshashar marked this conversation as resolved.
Show resolved Hide resolved
"""
Create a composite column that autogenerates HTML from Markdown text.

Creates two db columns named with ``_html`` and ``_text`` suffixes.

:param str name: Column name base
:param str profile: Config profile for the Markdown processor
:param bool deferred: Whether the columns should be deferred by default
:param str group: Defer column group
:param kwargs: Additional column options, passed to SQLAlchemy's column constructor
"""
# Construct a custom subclass of MarkdownComposite and set the markdown processor
# and processor options on it. We'll pass this class to SQLAlchemy's composite
# constructor.
class CustomMarkdownComposite(MarkdownCompositeNative):
pass

CustomMarkdownComposite.profile = profile
miteshashar marked this conversation as resolved.
Show resolved Hide resolved

return composite(
CustomMarkdownComposite,
miteshashar marked this conversation as resolved.
Show resolved Hide resolved
sa.Column(name + '_text', sa.UnicodeText, **kwargs),
sa.Column(name + '_html', sa.UnicodeText, **kwargs),
deferred=deferred,
group=group or name,
)


MarkdownColumnNative = markdown_column_native
miteshashar marked this conversation as resolved.
Show resolved Hide resolved
6 changes: 4 additions & 2 deletions funnel/models/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
BaseScopedIdNameMixin,
Commentset,
Mapped,
MarkdownColumn,
MarkdownColumnNative,
Project,
TimestampMixin,
TSVectorType,
Expand Down Expand Up @@ -101,7 +101,9 @@ class Update(UuidMixin, BaseScopedIdNameMixin, TimestampMixin, db.Model):
)
parent = sa.orm.synonym('project')

body = MarkdownColumn('body', nullable=False)
body = MarkdownColumnNative( # type: ignore[has-type]
'body', profile='basic', nullable=False
)

#: Update number, for Project updates, assigned when the update is published
number = with_roles(
Expand Down
17 changes: 14 additions & 3 deletions funnel/models/venue.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@

from coaster.sqlalchemy import add_primary_relationship, with_roles

from . import BaseScopedNameMixin, CoordinatesMixin, MarkdownColumn, UuidMixin, db, sa
from . import (
BaseScopedNameMixin,
CoordinatesMixin,
MarkdownColumnNative,
UuidMixin,
db,
sa,
)
from .helpers import reopen
from .project import Project
from .project_membership import project_child_role_map
Expand All @@ -25,7 +32,9 @@ class Venue(UuidMixin, BaseScopedNameMixin, CoordinatesMixin, db.Model):
grants_via={None: project_child_role_map},
)
parent = sa.orm.synonym('project')
description = MarkdownColumn('description', default='', nullable=False)
description = MarkdownColumnNative( # type: ignore[has-type]
'description', profile='basic', default='', nullable=False
miteshashar marked this conversation as resolved.
Show resolved Hide resolved
)
address1 = sa.Column(sa.Unicode(160), default='', nullable=False)
address2 = sa.Column(sa.Unicode(160), default='', nullable=False)
city = sa.Column(sa.Unicode(30), default='', nullable=False)
Expand Down Expand Up @@ -104,7 +113,9 @@ class VenueRoom(UuidMixin, BaseScopedNameMixin, db.Model):
grants_via={None: set(project_child_role_map.values())},
)
parent = sa.orm.synonym('venue')
description = MarkdownColumn('description', default='', nullable=False)
description = MarkdownColumnNative( # type: ignore[has-type]
'description', profile='basic', default='', nullable=False
)
bgcolor = sa.Column(sa.Unicode(6), nullable=False, default='229922')

seq = sa.Column(sa.Integer, nullable=False)
Expand Down
112 changes: 69 additions & 43 deletions funnel/utils/markdown/base.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
"""Base files for markdown parser."""
# pylint: disable=too-many-arguments

from typing import Dict, List, Optional, Union, overload
from typing import Any, Dict, List, Optional, Union, overload
import json

from markdown_it import MarkdownIt
from markupsafe import Markup

from coaster.utils.text import normalize_spaces_multiline

from .extmap import markdown_extensions
from .profiles import plugin_configs, plugins, profiles

__all__ = ['markdown']

Expand All @@ -32,63 +33,88 @@


@overload
def markdown(
text: None,
extensions: Union[List[str], None] = None,
extension_configs: Optional[Dict[str, str]] = None,
# TODO: Extend to accept helpers.EXT_CONFIG_TYPE (Dict)
) -> None:
def markdown(text: None, profile: Union[str, Dict[str, Any]]) -> None:
...


@overload
def markdown(
text: str,
extensions: Union[List[str], None] = None,
extension_configs: Optional[Dict[str, str]] = None,
# TODO: Extend to accept helpers.EXT_CONFIG_TYPE (Dict)
) -> Markup:
def markdown(text: str, profile: Union[str, Dict[str, Any]]) -> Markup:
...


def markdown(
text: Optional[str],
extensions: Union[List[str], None] = None,
extension_configs: Optional[Dict[str, str]] = None,
# TODO: Extend to accept helpers.EXT_CONFIG_TYPE (Dict)
text: Optional[str], profile: Union[str, Dict[str, Any]]
) -> Optional[Markup]:
"""
Markdown parser compliant with Commonmark+GFM using markdown-it-py.

:param bool linkify: Whether to convert naked URLs into links
:param list extensions: List of Markdown extensions to be enabled
:param dict extension_configs: Config for Markdown extensions
:param str|dict profile: Config profile to use
"""
if text is None:
return None

# Replace invisible characters with spaces
text = normalize_spaces_multiline(text)

md = MarkdownIt(
'gfm-like',
{
'breaks': True,
'linkify': True,
'typographer': True,
},
).enable(['smartquotes'])

md.linkify.set({'fuzzy_link': False, 'fuzzy_email': False})

if extensions is None:
extensions = default_markdown_extensions

for e in extensions:
if e in markdown_extensions:
ext_config = markdown_extensions[e].default_config
if extension_configs is not None and e in extension_configs:
ext_config = markdown_extensions[e].config(extension_configs[e])
md.use(markdown_extensions[e].ext, **ext_config)

return Markup(md.render(text)) # type: ignore[arg-type]
if isinstance(profile, str):
try:
_profile = profiles[profile]
except KeyError as exc:
raise KeyError(
f'Wrong markdown config profile "{profile}". Check name.'
) from exc
elif isinstance(profile, dict):
_profile = profile
else:
raise TypeError('Wrong type - profile has to be either str or dict')
miteshashar marked this conversation as resolved.
Show resolved Hide resolved

args = _profile.get('args', ())

md = MarkdownIt(*args)

funnel_config = _profile.get('funnel_config', {})
miteshashar marked this conversation as resolved.
Show resolved Hide resolved

if md.linkify is not None:
md.linkify.set({'fuzzy_link': False, 'fuzzy_email': False})

for action in ['enableOnly', 'enable', 'disable']:
if action in funnel_config:
getattr(md, action)(funnel_config[action])
miteshashar marked this conversation as resolved.
Show resolved Hide resolved

for e in _profile.get('plugins', []):
miteshashar marked this conversation as resolved.
Show resolved Hide resolved
try:
ext = plugins[e]
except KeyError as exc:
raise KeyError(
f'Wrong markdown-it-py plugin key "{e}". Check name.'
) from exc
md.use(ext, **plugin_configs.get(e, {}))

# type: ignore[arg-type]
return Markup(getattr(md, funnel_config.get('render_with', 'render'))(text))


def _print_rules(md: MarkdownIt, active: str = None):
"""Debug function to be removed before merge."""
rules = {'all_rules': md.get_all_rules(), 'active_rules': {}}
for p, pr in profiles.items():
m = MarkdownIt(*pr.get('args', ()))
fc = pr.get('funnel_config', {})
if m.linkify is not None:
m.linkify.set({'fuzzy_link': False, 'fuzzy_email': False})
for action in ['enableOnly', 'enable', 'disable']:
if action in fc:
getattr(m, action)(fc[action])
for e in pr.get('plugins', []):
try:
ext = plugins[e]
except KeyError as exc:
raise KeyError(
f'Wrong markdown-it-py plugin key "{e}". Check name.'
) from exc
m.use(ext, **plugin_configs.get(e, {}))
rules['active_rules'][p] = m.get_active_rules()
if active is not None:
print(json.dumps(rules['active_rules'][active], indent=2)) # noqa: T201
else:
print(json.dumps(rules, indent=2)) # noqa: T201
Loading