Compare commits

..

57 Commits

Author SHA1 Message Date
f4e6cd4ab0 Update README.md 2025-05-06 06:51:29 +00:00
c90d3071ea Update README.md 2025-05-06 06:51:06 +00:00
9de735aace Update package 2025-04-25 10:05:20 +02:00
786a72126c fix attribute error 2025-04-25 10:05:17 +02:00
70022a9b6c Update package 2025-04-25 10:03:02 +02:00
1f7b68e432 potential fix for same window preview 2025-04-25 10:02:58 +02:00
507a7e5d92 Update package 2025-04-25 10:00:08 +02:00
cc5d737c16 add logging for debugging 2025-04-25 10:00:03 +02:00
a7116d7206 Update package 2025-04-25 09:55:16 +02:00
4563a0f653 potentially fixes view in same window 2025-04-25 09:55:12 +02:00
e7a15ea070 Update package 2025-04-25 09:51:21 +02:00
34f8b3d733 open view in current window not in new one 2025-04-25 09:51:10 +02:00
e6a880d2a4 Merge branch 'master' of git.0x42.cloud:christian.morpurgo/MarkdownLivePreview 2025-04-24 20:33:02 +02:00
9cb5e2087f Update package 2025-04-24 20:32:52 +02:00
20dc7da4e8 Update package 2025-04-24 20:32:22 +02:00
0413ecca4b fix resourcxe path if repository name is different 2025-04-24 20:32:19 +02:00
37660abe64 Update repository.json 2025-04-24 18:29:07 +00:00
c2093e19aa Update package 2025-04-24 20:25:57 +02:00
b9ee2819e3 adjust scxale added more tags 2025-04-24 20:25:49 +02:00
99a4f21be3 Update package 2025-04-24 20:20:12 +02:00
edb424de99 font scale fix 2025-04-24 20:20:08 +02:00
d89e34f3e2 Update package 2025-04-24 20:18:52 +02:00
ecfd19dd85 potential fix for font scaling 2025-04-24 20:18:48 +02:00
69c722a933 Update package 2025-04-24 20:14:13 +02:00
7c37a9e413 potential fix for fontscaling 2025-04-24 20:14:10 +02:00
89d712fcce Update package 2025-04-24 20:11:22 +02:00
9f243f09c9 fix 2025-04-24 20:11:19 +02:00
ce768dce10 Update package 2025-04-24 20:09:01 +02:00
731f2def96 loggin 2025-04-24 20:08:56 +02:00
b3ffe8bf55 Update package 2025-04-24 20:05:55 +02:00
8576d4a631 Update package 2025-04-24 20:02:45 +02:00
49329d1f64 potential fix for font_scale 2025-04-24 20:02:39 +02:00
21b67f5b86 Update package 2025-04-24 20:00:49 +02:00
837979232e fix because we are using python 3.3 sic 2025-04-24 20:00:38 +02:00
547225ac4d Update package 2025-04-24 19:59:00 +02:00
e05516ab22 log font_scale 2025-04-24 19:58:47 +02:00
bbb90a8a97 Update package 2025-04-24 19:55:01 +02:00
598e22002b potential fix for font scale not working 2025-04-24 19:54:50 +02:00
e81b359294 Update package 2025-04-24 19:48:09 +02:00
8709e88fbd Update package 2025-04-24 19:46:51 +02:00
b94ad5856d fix import 2025-04-24 19:46:39 +02:00
3004ab4b41 Update package 2025-04-24 19:43:26 +02:00
14f6474cd5 fixing import error 2025-04-24 19:43:14 +02:00
d0323405c0 Update package 2025-04-24 19:40:38 +02:00
661f5b8911 fix impoert for soupsieve 2025-04-24 19:40:29 +02:00
3947b4cc4d Update package 2025-04-24 19:36:33 +02:00
2b35cf5000 add test_import to be ignored on export 2025-04-24 19:36:25 +02:00
72c684e89c Update package 2025-04-24 19:32:11 +02:00
ae1ea101d2 update package 2025-04-24 19:32:01 +02:00
3dd7b5a18d made bs4 and soupsieve standalone in this project 2025-04-24 19:26:20 +02:00
aefb27614f fully make bs4 and soupsieve standalone in the project 2025-04-24 17:39:41 +02:00
ed336866ee new build 2025-04-24 17:07:15 +02:00
afa554b52b Merge branch 'master' of git.0x42.cloud:christian.morpurgo/MarkdownLivePreview 2025-04-24 17:06:49 +02:00
e0daa147f1 fix import 2025-04-24 17:06:45 +02:00
f3cd6d3fcf Update repository.json 2025-04-24 15:04:58 +00:00
0034602fff new build 2025-04-24 17:04:04 +02:00
b29d054de2 new build 2025-04-24 16:49:41 +02:00
23 changed files with 369 additions and 116 deletions

2
.gitattributes vendored
View File

@ -1,3 +1,5 @@
docs/ export-ignore
resources/
!resources/*.base64
test_imports.py export-ignore
MarkdownLivePreview.sublime-package export-ignore

View File

@ -1,8 +1,21 @@
import os, sys, sublime
pkg = os.path.basename(os.path.dirname(__file__))
lib = os.path.join(sublime.packages_path(), pkg, "lib")
if lib not in sys.path:
sys.path.insert(0, lib)
import os
import sys
import sublime
# Add the package archive path itself to sys.path
package_path = os.path.dirname(__file__)
if package_path not in sys.path:
sys.path.insert(0, package_path)
# --- Add lib to sys.path ---
# Get the directory containing this file (MarkdownLivePreview.py)
plugin_dir = os.path.dirname(__file__)
# Construct the absolute path to the 'lib' directory
lib_path = os.path.join(plugin_dir, 'lib')
# Add it to the beginning of sys.path if it's not already there
if lib_path not in sys.path:
sys.path.insert(0, lib_path)
# --- End sys.path modification ---
"""
Terminology
@ -38,7 +51,7 @@ def plugin_loaded():
resources["stylesheet"] = get_resource("stylesheet.css")
# FIXME: how could we make this setting update without restarting sublime text
# and not loading it every update as well
DELAY = get_settings().get(SETTING_DELAY_BETWEEN_UPDATES)
DELAY = get_settings().get(SETTING_DELAY_BETWEEN_UPDATES, 100) # Provide default
class MdlpInsertCommand(sublime_plugin.TextCommand):
@ -49,6 +62,7 @@ class MdlpInsertCommand(sublime_plugin.TextCommand):
class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand):
def run(self, edit):
print("--- MarkdownLivePreview: OpenMarkdownPreviewCommand running ---")
""" If the file is saved exists on disk, we close it, and reopen it in a new
window. Otherwise, we copy the content, erase it all (to close the file without
a dialog) and re-insert it into a new view into a new window """
@ -56,22 +70,30 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand):
original_view = self.view
original_window_id = original_view.window().id()
file_name = original_view.file_name()
print("--- MarkdownLivePreview: Original view ID: {}, File: {}, Window ID: {} ---".format(original_view.id(), file_name, original_window_id))
syntax_file = original_view.settings().get("syntax")
if file_name:
original_view.close()
else:
# the file isn't saved, we need to restore the content manually
# don't close the original view; keep it in your main window:
# if file_name:
# original_view.close()
# else:
# # the file isn't saved, we need to restore the content manually
# total_region = sublime.Region(0, original_view.size())
# content = original_view.substr(total_region)
# original_view.erase(edit, total_region)
# original_view.close()
# # FIXME: save the document to a temporary file, so that if we crash,
# # the user doesn't lose what he wrote
if not file_name:
# If the file isn't saved, we still need the content for the new view
total_region = sublime.Region(0, original_view.size())
content = original_view.substr(total_region)
original_view.erase(edit, total_region)
original_view.close()
# FIXME: save the document to a temporary file, so that if we crash,
# the user doesn't lose what he wrote
print("--- MarkdownLivePreview: Unsaved file, content length: {} ---".format(len(content)))
sublime.run_command("new_window")
preview_window = sublime.active_window()
# instead of making a new window, grab your existing one:
preview_window = original_view.window()
print("--- MarkdownLivePreview: Using existing window ID: {} ---".format(preview_window.id()))
preview_window.run_command(
"set_layout",
@ -87,20 +109,44 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand):
preview_view.set_scratch(True)
preview_view.settings().set(PREVIEW_VIEW_INFOS, {})
preview_view.set_name("Preview")
print("--- MarkdownLivePreview: Created preview_view ID: {} ---".format(preview_view.id()))
# FIXME: hide number lines on preview
preview_window.focus_group(0)
if file_name:
markdown_view = preview_window.open_file(file_name)
print("--- MarkdownLivePreview: Opened markdown_view ID: {} for file: {} ---".format(markdown_view.id(), file_name))
else:
markdown_view = preview_window.new_file()
markdown_view.run_command("mdlp_insert", {"point": 0, "string": content})
markdown_view.set_scratch(True)
print("--- MarkdownLivePreview: Created new markdown_view ID: {} for unsaved content ---".format(markdown_view.id()))
markdown_view.set_syntax_file(syntax_file)
markdown_view.settings().set(
MARKDOWN_VIEW_INFOS, {"original_window_id": original_window_id,},
MARKDOWN_VIEW_INFOS, {"original_window_id": original_window_id, "preview_view_id": preview_view.id(),},
)
infos_to_log = markdown_view.settings().get(MARKDOWN_VIEW_INFOS)
print("--- MarkdownLivePreview: Stored infos on markdown_view {}: {} ---".format(markdown_view.id(), infos_to_log))
# Manually trigger the initial setup and render via the listener
# Use set_timeout_async to run it after the command finishes
print("--- MarkdownLivePreview: Scheduling setup_and_update_preview for md_view: {}, pv_view: {} ---".format(markdown_view.id(), preview_view.id()))
sublime.set_timeout_async(lambda: self.trigger_listener_setup(markdown_view.id(), preview_view.id()), 0)
# Helper method to find and call the listener instance method
# This is needed because the listener isn't easily accessible directly from the command instance
def trigger_listener_setup(self, md_view_id, pv_view_id):
print("--- MarkdownLivePreview: trigger_listener_setup running for md_view: {}, pv_view: {} ---".format(md_view_id, pv_view_id))
# Access the listener instance directly via its class variable
listener_instance = MarkdownLivePreviewListener.instance
if listener_instance:
print("--- MarkdownLivePreview: Found listener instance via class variable, calling setup_and_update_preview ---")
listener_instance.setup_and_update_preview(md_view_id, pv_view_id)
else:
# This should ideally not happen if the listener loaded correctly before the command ran
print("--- MarkdownLivePreview: ERROR: MarkdownLivePreviewListener.instance is None! Cannot trigger setup. ---")
def is_enabled(self):
# FIXME: is this the best way there is to check if the current syntax is markdown?
@ -111,6 +157,7 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand):
class MarkdownLivePreviewListener(sublime_plugin.EventListener):
instance = None # Class variable to hold the single instance
phantom_sets = {
# markdown_view.id(): phantom set
@ -120,6 +167,11 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener):
# then, we update only if now() - last_update > DELAY
last_update = 0
def __init__(self):
super().__init__() # Good practice to call super
MarkdownLivePreviewListener.instance = self
print("--- MarkdownLivePreview: Listener instance created and registered. ---")
# FIXME: maybe we shouldn't restore the file in the original window...
def on_pre_close(self, markdown_view):
@ -136,18 +188,30 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener):
if self.file_name is None:
total_region = sublime.Region(0, markdown_view.size())
self.content = markdown_view.substr(total_region)
markdown_view.erase(edit, total_region)
else:
self.content = None
def on_load_async(self, markdown_view):
infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS)
if not infos:
# print("--- MarkdownLivePreview: on_load_async ignored for view {} - no infos ---".format(markdown_view.id())) # Optional: very verbose
return
preview_view = markdown_view.window().active_view_in_group(1)
print("--- MarkdownLivePreview: on_load_async triggered for markdown_view {} ---".format(markdown_view.id()))
preview_view_id = infos.get("preview_view_id")
if not preview_view_id:
print("--- MarkdownLivePreview: ERROR in on_load_async: No preview_view_id found in infos: {} ---".format(infos))
return # Should not happen if setup was correct
self.phantom_sets[markdown_view.id()] = sublime.PhantomSet(preview_view)
preview_view = sublime.View(preview_view_id)
if not preview_view.is_valid():
print("--- MarkdownLivePreview: ERROR in on_load_async: Preview view {} is no longer valid ---".format(preview_view_id))
return # Preview view was closed before loading finished
print("--- MarkdownLivePreview: on_load_async found valid preview_view {} ---".format(preview_view.id()))
# PhantomSet creation is now handled by setup_and_update_preview triggered from the command
# self.phantom_sets[markdown_view.id()] = sublime.PhantomSet(preview_view)
# print("--- MarkdownLivePreview: PhantomSet created in on_load_async for preview_view {} ---".format(preview_view.id())) # Keep log commented
self._update_preview(markdown_view)
def on_close(self, markdown_view):
@ -157,13 +221,11 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener):
if not infos:
return
assert (
markdown_view.id() == self.markdown_view.id()
), "pre_close view.id() != close view.id()"
if markdown_view.id() in self.phantom_sets:
del self.phantom_sets[markdown_view.id()]
self.preview_window.run_command("close_window")
# don't close the entire window—just let the user close the preview tab:
# self.preview_window.run_command("close_window")
# find the window with the right id
original_window = next(
@ -194,14 +256,17 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener):
# @min_time_between_call(.5)
def on_modified_async(self, markdown_view):
# print("--- MarkdownLivePreview: on_modified_async triggered for view {} ---".format(markdown_view.id())) # Optional: very verbose
infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS)
if not infos:
return
print("--- MarkdownLivePreview: Scheduling update for markdown_view {} ---".format(markdown_view.id()))
# we schedule an update, which won't run if an
sublime.set_timeout(partial(self._update_preview, markdown_view), DELAY)
def _update_preview(self, markdown_view):
print("--- MarkdownLivePreview: _update_preview called for markdown_view {} ---".format(markdown_view.id()))
# if the buffer id is 0, that means that the markdown_view has been closed
# This check is needed since a this function is used as a callback for when images
# are loaded from the internet (ie. it could finish loading *after* the user
@ -210,25 +275,53 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener):
settings = get_settings()
delay = settings.get(SETTING_DELAY_BETWEEN_UPDATES, 100) # Provide default
font_scale = settings.get(SETTING_FONT_SCALE, 1.0) # Provide default
print("--- MarkdownLivePreview: Using font_scale: {} ---".format(font_scale))
if time.time() - self.last_update < delay / 1000:
print("--- MarkdownLivePreview: Update skipped for view {} due to time delay ---".format(markdown_view.id()))
return
if markdown_view.buffer_id() == 0:
print("--- MarkdownLivePreview: Update skipped for view {}: buffer_id is 0 (view closed) ---".format(markdown_view.id()))
return
# Check if the phantom set still exists for this view ID
if markdown_view.id() not in self.phantom_sets:
print("--- MarkdownLivePreview: Update skipped for view {}: No phantom set found ---".format(markdown_view.id()))
# View might have been closed between modification and update
return
print("--- MarkdownLivePreview: Update proceeding for view {} ---".format(markdown_view.id()))
self.last_update = time.time()
total_region = sublime.Region(0, markdown_view.size())
markdown = markdown_view.substr(total_region)
print("--- MarkdownLivePreview: Read markdown content (length: {}) ---".format(len(markdown)))
preview_view = markdown_view.window().active_view_in_group(1)
infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS)
if not infos:
print("--- MarkdownLivePreview: ERROR in _update_preview: No infos found for view {} ---".format(markdown_view.id()))
return # Should not happen
preview_view_id = infos.get("preview_view_id")
if not preview_view_id:
print("--- MarkdownLivePreview: ERROR in _update_preview: No preview_view_id found in infos: {} ---".format(infos))
return # Should not happen
preview_view = sublime.View(preview_view_id)
if not preview_view.is_valid():
print("--- MarkdownLivePreview: ERROR in _update_preview: Preview view {} is no longer valid ---".format(preview_view_id))
return # Preview view was closed
print("--- MarkdownLivePreview: Found valid preview_view {} for update ---".format(preview_view.id()))
# Get viewport_width, default to a large value if view isn't ready
viewport_extent = preview_view.viewport_extent()
viewport_width = viewport_extent[0] if viewport_extent else 1024
print("--- MarkdownLivePreview: Viewport width: {} ---".format(viewport_width))
basepath = os.path.dirname(markdown_view.file_name()) if markdown_view.file_name() else '.' # Handle unsaved files
print("--- MarkdownLivePreview: Calling markdown2html with basepath: {} ---".format(basepath))
html = markdown2html(
markdown,
basepath,
@ -238,6 +331,10 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener):
font_scale,
)
# Truncate HTML safely for logging
html_preview = html[:100].replace('\n', ' ') # Avoid breaking log lines
print("--- MarkdownLivePreview: Generated HTML (starts with): {}... ---".format(html_preview))
self.phantom_sets[markdown_view.id()].update(
[
sublime.Phantom(
@ -248,6 +345,32 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener):
)
]
)
print("--- MarkdownLivePreview: Updated phantoms in preview_view {} ---".format(preview_view.id()))
def setup_and_update_preview(self, markdown_view_id, preview_view_id):
print("--- MarkdownLivePreview: setup_and_update_preview called for md_view: {}, pv_view: {} ---".format(markdown_view_id, preview_view_id))
markdown_view = sublime.View(markdown_view_id)
preview_view = sublime.View(preview_view_id)
if not markdown_view.is_valid():
print("--- MarkdownLivePreview: ERROR in setup_and_update: markdown_view {} is not valid ---".format(markdown_view_id))
return
if not preview_view.is_valid():
print("--- MarkdownLivePreview: ERROR in setup_and_update: preview_view {} is not valid ---".format(preview_view_id))
return
# Create PhantomSet if it doesn't exist (shouldn't at this point)
if markdown_view_id not in self.phantom_sets:
print("--- MarkdownLivePreview: Creating PhantomSet for preview_view {} in setup ---".format(preview_view.id()))
self.phantom_sets[markdown_view_id] = sublime.PhantomSet(preview_view)
else:
# This case might occur if the command is run multiple times rapidly? Ensure it's associated correctly.
print("--- MarkdownLivePreview: Warning: PhantomSet already existed for markdown_view {} in setup. Re-associating with preview_view {}. ---".format(markdown_view_id, preview_view.id()))
self.phantom_sets[markdown_view_id].view = preview_view # Ensure it points to the correct view
# Trigger the first update
print("--- MarkdownLivePreview: Triggering initial _update_preview from setup ---")
self._update_preview(markdown_view)
def get_settings():
@ -255,11 +378,15 @@ def get_settings():
def get_resource(resource):
path = "Packages/MarkdownLivePreview/resources/" + resource
abs_path = os.path.join(sublime.packages_path(), "..", path)
package_name = __package__ # Get the current package name dynamically
path = "Packages/{}/resources/{}".format(package_name, resource)
# Original logic: check absolute path first (useful for unpacked development)
# Adjusted abs_path to use dynamic package_name
abs_path = os.path.join(sublime.packages_path(), package_name, "resources", resource)
if os.path.isfile(abs_path):
with open(abs_path, "r") as fp:
return fp.read()
# Fallback to sublime.load_resource (works for packed and unpacked)
return sublime.load_resource(path)

Binary file not shown.

View File

@ -1,5 +1,25 @@
# MarkdownLivePreview
## Acknowledgments
This project is a fork of [MarkdownLivePreview](https://github.com/math2001/MarkdownLivePreview) by **math2001**. I'm grateful for the original implementation, which provided a solid foundation for live Markdown preview in Sublime Text.
Many thanks to math2001 for the original code—this fork wouldn't have been possible without their work.
## Changes contained in this Fork
In this fork ([christian.morpurgo/MarkdownLivePreview](https://git.0x42.cloud/christian.morpurgo/MarkdownLivePreview)), I've made the following enhancements:
- **Bundled `bs4` and `soupsieve`**
Repackaged both libraries as standalone modules by rewriting their imports, resolving errors when Sublime Text 4 attempted to install these old versions as external dependencies.
- **`font_scale` option**
Added a new `font_scale: number` setting to allow users to increase or decrease the preview text size directly from their Sublime Text settings.
- **In-window preview**
Changed the preview behavior so that starting a preview reuses the current window instead of opening a new one.
A simple plugin to preview your markdown as you type right in Sublime Text.
No dependencies!
@ -31,7 +51,7 @@ could be working on, then there are a bunch of `FIXME`s all over this package.
Just pick one and fix it :-)
```
$ git clone https://github.com/math2001/MarkdownLivePreview
$ git clone https://git.0x42.cloud/christian.morpurgo/MarkdownLivePreview
$ cd MarkdownLivePreview
$ grep -R FIXME
```

6
create_package.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
git archive --format=zip --prefix="" --output=MarkdownLivePreview.sublime-package master
git add MarkdownLivePreview.sublime-package
git commit -m "Update package"
git push origin master

View File

@ -4,7 +4,7 @@ __license__ = "MIT"
from collections import defaultdict
import itertools
import sys
from bs4.element import (
from ..element import (
CharsetMetaAttributeValue,
ContentMetaAttributeValue,
Stylesheet,

View File

@ -7,13 +7,13 @@ __all__ = [
import warnings
import re
from bs4.builder import (
from . import (
PERMISSIVE,
HTML,
HTML_5,
HTMLTreeBuilder,
)
from bs4.element import (
from ..element import (
NamespacedAttribute,
nonwhitespace_re,
)
@ -22,7 +22,7 @@ from html5lib.constants import (
namespaces,
prefixes,
)
from bs4.element import (
from ..element import (
Comment,
Doctype,
NavigableString,
@ -120,7 +120,7 @@ class TreeBuilderForHtml5lib(treebuilder_base.TreeBuilder):
if soup:
self.soup = soup
else:
from bs4 import BeautifulSoup
from .. import BeautifulSoup
# TODO: Why is the parser 'html.parser' here? To avoid an
# infinite loop?
self.soup = BeautifulSoup(
@ -166,7 +166,7 @@ class TreeBuilderForHtml5lib(treebuilder_base.TreeBuilder):
return TextNode(Comment(data), self.soup)
def fragmentClass(self):
from bs4 import BeautifulSoup
from .. import BeautifulSoup
# TODO: Why is the parser 'html.parser' here? To avoid an
# infinite loop?
self.soup = BeautifulSoup("", "html.parser")
@ -184,7 +184,7 @@ class TreeBuilderForHtml5lib(treebuilder_base.TreeBuilder):
return treebuilder_base.TreeBuilder.getFragment(self).element
def testSerializer(self, element):
from bs4 import BeautifulSoup
from .. import BeautifulSoup
rv = []
doctype_re = re.compile(r'^(.*?)(?: PUBLIC "(.*?)"(?: "(.*?)")?| SYSTEM "(.*?)")?$')

View File

@ -34,16 +34,16 @@ CONSTRUCTOR_STRICT_IS_DEPRECATED = major == 3 and minor == 3
CONSTRUCTOR_TAKES_CONVERT_CHARREFS = major == 3 and minor >= 4
from bs4.element import (
from ..element import (
CData,
Comment,
Declaration,
Doctype,
ProcessingInstruction,
)
from bs4.dammit import EntitySubstitution, UnicodeDammit
from ..dammit import EntitySubstitution, UnicodeDammit
from bs4.builder import (
from . import (
HTML,
HTMLTreeBuilder,
STRICT,

View File

@ -14,14 +14,14 @@ except ImportError as e:
from io import BytesIO
from io import StringIO
from lxml import etree
from bs4.element import (
from ..element import (
Comment,
Doctype,
NamespacedAttribute,
ProcessingInstruction,
XMLProcessingInstruction,
)
from bs4.builder import (
from . import (
FAST,
HTML,
HTMLTreeBuilder,
@ -29,7 +29,7 @@ from bs4.builder import (
ParserRejectedMarkup,
TreeBuilder,
XML)
from bs4.dammit import EncodingDetector
from ..dammit import EncodingDetector
LXML = 'lxml'

View File

@ -6,9 +6,9 @@ __license__ = "MIT"
import cProfile
from io import StringIO
from html.parser import HTMLParser
import bs4
from bs4 import BeautifulSoup, __version__
from bs4.builder import builder_registry
from . import BeautifulSoup as bs4
from . import BeautifulSoup, __version__
from .builder import builder_registry
import os
import pstats

View File

@ -9,14 +9,16 @@ import re
import sys
import warnings
try:
import soupsieve
# We are installed under the bs4 package
from soupsieve import *
except ImportError as e:
# We are installed standalone, or soupsieve is not installed.
soupsieve = None
warnings.warn(
'The soupsieve package is not installed. CSS selectors cannot be used.'
)
from bs4.formatter import (
from .formatter import (
Formatter,
HTMLFormatter,
XMLFormatter,
@ -380,7 +382,7 @@ class PageElement(object):
and not isinstance(new_child, NavigableString)):
new_child = NavigableString(new_child)
from bs4 import BeautifulSoup
from . import BeautifulSoup
if isinstance(new_child, BeautifulSoup):
# We don't want to end up with a situation where one BeautifulSoup
# object contains another. Insert the children one at a time.

View File

@ -1,4 +1,4 @@
from bs4.dammit import EntitySubstitution
from .dammit import EntitySubstitution
class Formatter(EntitySubstitution):
"""Describes a strategy to use when outputting a parse tree to a string.

View File

@ -9,8 +9,8 @@ import copy
import functools
import unittest
from unittest import TestCase
from bs4 import BeautifulSoup
from bs4.element import (
from . import BeautifulSoup
from .element import (
CharsetMetaAttributeValue,
Comment,
ContentMetaAttributeValue,
@ -22,7 +22,7 @@ from bs4.element import (
Tag
)
from bs4.builder import HTMLParserTreeBuilder
from .builder import HTMLParserTreeBuilder
default_builder = HTMLParserTreeBuilder
BAD_DOCUMENT = """A bare string

View File

@ -3,21 +3,21 @@
import unittest
import warnings
from bs4 import BeautifulSoup
from bs4.builder import (
from .. import BeautifulSoup
from ..builder import (
builder_registry as registry,
HTMLParserTreeBuilder,
TreeBuilderRegistry,
)
try:
from bs4.builder import HTML5TreeBuilder
from ..builder import HTML5TreeBuilder
HTML5LIB_PRESENT = True
except ImportError:
HTML5LIB_PRESENT = False
try:
from bs4.builder import (
from ..builder import (
LXMLTreeBuilderForXML,
LXMLTreeBuilder,
)

View File

@ -3,12 +3,12 @@
import warnings
try:
from bs4.builder import HTML5TreeBuilder
from ..builder import HTML5TreeBuilder
HTML5LIB_PRESENT = True
except ImportError as e:
HTML5LIB_PRESENT = False
from bs4.element import SoupStrainer
from bs4.testing import (
from ..element import SoupStrainer
from ..testing import (
HTML5TreeBuilderSmokeTest,
SoupTest,
skipIf,

View File

@ -3,9 +3,9 @@ trees."""
from pdb import set_trace
import pickle
from bs4.testing import SoupTest, HTMLTreeBuilderSmokeTest
from bs4.builder import HTMLParserTreeBuilder
from bs4.builder._htmlparser import BeautifulSoupHTMLParser
from ..testing import SoupTest, HTMLTreeBuilderSmokeTest
from ..builder import HTMLParserTreeBuilder
from ..builder._htmlparser import BeautifulSoupHTMLParser
class HTMLParserTreeBuilderSmokeTest(SoupTest, HTMLTreeBuilderSmokeTest):

View File

@ -12,16 +12,16 @@ except ImportError as e:
LXML_VERSION = (0,)
if LXML_PRESENT:
from bs4.builder import LXMLTreeBuilder, LXMLTreeBuilderForXML
from ..builder import LXMLTreeBuilder, LXMLTreeBuilderForXML
from bs4 import (
from .. import (
BeautifulSoup,
BeautifulStoneSoup,
)
from bs4.element import Comment, Doctype, SoupStrainer
from bs4.testing import skipIf
from bs4.tests import test_htmlparser
from bs4.testing import (
from ..element import Comment, Doctype, SoupStrainer
from ..testing import skipIf
from . import test_htmlparser
from ..testing import (
HTMLTreeBuilderSmokeTest,
XMLTreeBuilderSmokeTest,
SoupTest,

View File

@ -7,17 +7,17 @@ import unittest
import sys
import tempfile
from bs4 import (
from .. import (
BeautifulSoup,
BeautifulStoneSoup,
GuessedAtParserWarning,
MarkupResemblesLocatorWarning,
)
from bs4.builder import (
from ..builder import (
TreeBuilder,
ParserRejectedMarkup,
)
from bs4.element import (
from ..element import (
CharsetMetaAttributeValue,
Comment,
ContentMetaAttributeValue,
@ -27,13 +27,13 @@ from bs4.element import (
NavigableString,
)
import bs4.dammit
from bs4.dammit import (
from ..dammit import *
from ..dammit import (
EntitySubstitution,
UnicodeDammit,
EncodingDetector,
)
from bs4.testing import (
from ..testing import (
default_builder,
SoupTest,
skipIf,
@ -41,7 +41,7 @@ from bs4.testing import (
import warnings
try:
from bs4.builder import LXMLTreeBuilder, LXMLTreeBuilderForXML
from ..builder import LXMLTreeBuilder, LXMLTreeBuilderForXML
LXML_PRESENT = True
except ImportError as e:
LXML_PRESENT = False
@ -120,11 +120,10 @@ class TestConstructor(SoupTest):
def feed(self, *args, **kwargs):
raise ParserRejectedMarkup("Nope.")
def prepare_markup(self, *args, **kwargs):
def prepare_markup(self, markup, *args, **kwargs):
# We're going to try two different ways of preparing this markup,
# but feed() will reject both of them.
yield markup, None, None, False
yield markup, None, None, False
import re
self.assertRaisesRegex(
@ -418,13 +417,13 @@ class TestEncodingConversion(SoupTest):
def test_ascii_in_unicode_out(self):
# ASCII input is converted to Unicode. The original_encoding
# attribute is set to 'utf-8', a superset of ASCII.
chardet = bs4.dammit.chardet_dammit
chardet = chardet_dammit
logging.disable(logging.WARNING)
try:
def noop(str):
return None
# Disable chardet, which will realize that the ASCII is ASCII.
bs4.dammit.chardet_dammit = noop
chardet_dammit = noop
ascii = b"<foo>a</foo>"
soup_from_ascii = self.soup(ascii)
unicode_output = soup_from_ascii.decode()
@ -433,7 +432,7 @@ class TestEncodingConversion(SoupTest):
self.assertEqual(soup_from_ascii.original_encoding.lower(), "utf-8")
finally:
logging.disable(logging.NOTSET)
bs4.dammit.chardet_dammit = chardet
chardet_dammit = chardet
def test_unicode_in_unicode_out(self):
# Unicode input is left alone. The original_encoding attribute
@ -574,12 +573,12 @@ class TestUnicodeDammit(unittest.TestCase):
doc = b"""\357\273\277<?xml version="1.0" encoding="UTF-8"?>
<html><b>\330\250\330\252\330\261</b>
<i>\310\322\321\220\312\321\355\344</i></html>"""
chardet = bs4.dammit.chardet_dammit
chardet = chardet_dammit
logging.disable(logging.WARNING)
try:
def noop(str):
return None
bs4.dammit.chardet_dammit = noop
chardet_dammit = noop
dammit = UnicodeDammit(doc)
self.assertEqual(True, dammit.contains_replacement_characters)
self.assertTrue("\ufffd" in dammit.unicode_markup)
@ -588,7 +587,7 @@ class TestUnicodeDammit(unittest.TestCase):
self.assertTrue(soup.contains_replacement_characters)
finally:
logging.disable(logging.NOTSET)
bs4.dammit.chardet_dammit = chardet
chardet_dammit = chardet
def test_byte_order_mark_removed(self):
# A document written in UTF-16LE will have its byte order marker stripped.
@ -613,7 +612,7 @@ class TestUnicodeDammit(unittest.TestCase):
self.assertRaises(UnicodeDecodeError, doc.decode, "utf8")
# Unicode, Dammit thinks the whole document is Windows-1252,
# and decodes it into "☃☃☃Hi, I like Windows!☃☃☃"
# and decodes it into "☃☃☃"Hi, I like Windows!"☃☃☃"
# But if we run it through fix_embedded_windows_1252, it's fixed:

View File

@ -14,12 +14,12 @@ import copy
import pickle
import re
import warnings
from bs4 import BeautifulSoup
from bs4.builder import (
from .. import BeautifulSoup
from ..builder import (
builder_registry,
HTMLParserTreeBuilder,
)
from bs4.element import (
from ..element import (
PY3K,
CData,
Comment,
@ -33,11 +33,11 @@ from bs4.element import (
Tag,
TemplateString,
)
from bs4.testing import (
from ..testing import (
SoupTest,
skipIf,
)
from soupsieve import SelectorSyntaxError
from ...soupsieve import SelectorSyntaxError
XML_BUILDER_PRESENT = (builder_registry.lookup("xml") is not None)
LXML_PRESENT = (builder_registry.lookup("lxml") is not None)

View File

@ -6,10 +6,13 @@ from .import css_types as ct
import unicodedata
from collections.abc import Sequence
import bs4
from ..bs4 import BeautifulSoup
from ..bs4.element import (
Tag, NavigableString, Comment, Declaration, CData, ProcessingInstruction, Doctype
)
# Empty tag pattern (whitespace okay)
RE_NOT_EMPTY = re.compile('[^ \t\r\n\f]')
RE_NOT_EMPTY = re.compile(r'[^ \t\r\n\f]')
RE_NOT_WS = re.compile('[^ \t\r\n\f]+')
@ -90,37 +93,37 @@ class _DocumentNav(object):
@staticmethod
def is_doc(obj):
"""Is `BeautifulSoup` object."""
return isinstance(obj, bs4.BeautifulSoup)
return isinstance(obj, BeautifulSoup)
@staticmethod
def is_tag(obj):
"""Is tag."""
return isinstance(obj, bs4.Tag)
return isinstance(obj, Tag)
@staticmethod
def is_declaration(obj): # pragma: no cover
"""Is declaration."""
return isinstance(obj, bs4.Declaration)
return isinstance(obj, Declaration)
@staticmethod
def is_cdata(obj):
"""Is CDATA."""
return isinstance(obj, bs4.CData)
return isinstance(obj, CData)
@staticmethod
def is_processing_instruction(obj): # pragma: no cover
"""Is processing instruction."""
return isinstance(obj, bs4.ProcessingInstruction)
return isinstance(obj, ProcessingInstruction)
@staticmethod
def is_navigable_string(obj):
"""Is navigable string."""
return isinstance(obj, bs4.NavigableString)
return isinstance(obj, NavigableString)
@staticmethod
def is_special_string(obj):
"""Is special string."""
return isinstance(obj, (bs4.Comment, bs4.Declaration, bs4.CData, bs4.ProcessingInstruction, bs4.Doctype))
return isinstance(obj, (Comment, Declaration, CData, ProcessingInstruction, Doctype))
@classmethod
def is_content_string(cls, obj):

View File

@ -11,11 +11,12 @@ import os.path
import concurrent.futures
import urllib.request
import base64
import bs4
from .lib.bs4 import BeautifulSoup as bs4
from .lib.bs4.element import Comment
from functools import partial
from markdown2 import Markdown
from .lib.markdown2 import Markdown
__all__ = ("markdown2html",)
@ -33,7 +34,7 @@ def markdown2html(markdown, basepath, re_render, resources, viewport_width, font
"""
html = markdowner.convert(markdown)
soup = bs4.BeautifulSoup(html, "html.parser")
soup = bs4(html, "html.parser")
for img_element in soup.find_all("img"):
src = img_element["src"]
@ -61,7 +62,7 @@ def markdown2html(markdown, basepath, re_render, resources, viewport_width, font
# remove comments, because they pollute the console with error messages
for comment_element in soup.find_all(
text=lambda text: isinstance(text, bs4.Comment)
text=lambda text: isinstance(text, Comment)
):
comment_element.extract()
@ -79,14 +80,61 @@ def markdown2html(markdown, basepath, re_render, resources, viewport_width, font
.replace("\n", "<br />")
)
code_element.replace_with(bs4.BeautifulSoup(fixed_pre, "html.parser"))
code_element.replace_with(bs4(fixed_pre, "html.parser"))
# FIXME: highlight the code using Sublime's syntax
# Apply font scaling via inline styles
if font_scale != 1.0:
BASE_PX_SIZE = 15 # Base font size in pixels
TAG_MULTIPLIERS = {
'p': 1.0,
'li': 1.0,
'h1': 2.0,
'h2': 1.8,
'h3': 1.6,
'h4': 1.4,
'h5': 1.2,
'h6': 1.1,
'blockquote': 1.0,
'td': 1.0,
'th': 1.0,
'dt': 1.0,
'dd': 1.0,
'table': 1.0,
'tr': 1.0,
'ul': 1.0,
'ol': 1.0,
'code': 1.0,
'pre': 1.0,
'a': 1.0,
'strong': 1.0,
'em': 1.0,
's': 1.0,
'sup': 1.0,
'sub': 1.0,
'mark': 1.0,
'small': 1.0,
'big': 1.0,
'kbd': 1.0,
'samp': 1.0,
'var': 1.0,
'cite': 1.0,
'dfn': 1.0,
'abbr': 1.0,
'acronym': 1.0,
}
# Find all tags that we want to scale
for element in soup.find_all(list(TAG_MULTIPLIERS.keys())):
multiplier = TAG_MULTIPLIERS.get(element.name, 1.0)
target_size = round(BASE_PX_SIZE * multiplier * font_scale)
# Simple style setting (overwrites existing inline style if any)
# A more robust solution would parse and merge existing styles
element['style'] = "font-size: {}px;".format(target_size)
# FIXME: report that ST doesn't support <br/> but does work with <br />... WTF?
# Add font scaling CSS rule
font_scale_css = "body {{ font-size: {}em; }}\n".format(font_scale)
stylesheet = font_scale_css + resources["stylesheet"]
stylesheet = resources["stylesheet"] # Use only the base stylesheet
return "<style>\n{}\n</style>\n\n{}".format(stylesheet, soup).replace(
"<br/>", "<br />"
@ -194,13 +242,19 @@ def get_image_size(fhandle, pathlike):
fhandle.seek(size, 1)
byte = fhandle.read(1)
if byte == b"":
fhandle = end
byte = fhandle.read(1)
# Reached end of file unexpectedly, break the loop
break
while ord(byte) == 0xFF:
byte = fhandle.read(1)
if byte == b"": # Check EOF in inner loop too
break
if byte == b"": # Break outer loop if inner loop hit EOF
break
ftype = ord(byte)
size = struct.unpack(">H", fhandle.read(2))[0] - 2
# Check if the loop exited because of a break (EOF) before finding the marker
if not (0xC0 <= ftype <= 0xCF):
return "unknown format {!r}".format(format_)
# We are at a SOFn block
fhandle.seek(1, 1) # Skip `precision' byte.
height, width = struct.unpack(">HH", fhandle.read(4))

View File

@ -2,14 +2,14 @@
"schema_version": "3.0.0",
"packages": [
{
"name": "MarkdownLivePreview",
"name": "MarkdownLivePreview-FORK",
"description": "My enhanced live-preview fork of MarkdownLivePreview",
"author": "Christian Morpurgo",
"homepage": "https://git.0x42.cloud/christian.morpurgo/MarkdownLivePreview",
"releases": [
{
"version": "6.0.1",
"url": "https://git.0x42.cloud/christian.morpurgo/MarkdownLivePreview/releases/download/v6.0.1/MarkdownLivePreview.sublime-package",
"version": "6.0.2",
"url": "https://git.0x42.cloud/christian.morpurgo/MarkdownLivePreview/releases/download/v6.0.2/MarkdownLivePreview.sublime-package",
"date": "2025-04-24 00:00:00",
"sublime_text": "*"
}

40
test_imports.py Normal file
View File

@ -0,0 +1,40 @@
# test_imports.py
import sys
import os
# Optional: Explicitly add project root to path if needed,
# although running from the root often suffices.
# project_root = os.path.dirname(__file__)
# if project_root not in sys.path:
# sys.path.insert(0, project_root)
print("Attempting imports...")
try:
# Try importing the main entry point for bs4 from the lib structure
from lib.bs4 import BeautifulSoup
print("- Successfully imported BeautifulSoup from lib.bs4")
# Try creating a simple soup object (tests basic bs4 internal imports)
soup = BeautifulSoup("<a></a>", "html.parser")
print(f"- Created soup object: {soup.a}")
# Try importing the main entry point for soupsieve
from lib.soupsieve import compile as soupsieve_compile
print("- Successfully imported compile from lib.soupsieve")
# Try compiling a simple selector (tests basic soupsieve internal imports)
compiled = soupsieve_compile("a")
print(f"- Compiled selector: {compiled.pattern}")
# Try using the selector (tests soupsieve -> bs4 interaction)
match = compiled.select_one(soup)
print(f"- Selector match: {match}")
print("\nBasic import and usage tests passed!")
except ImportError as e:
print(f"\nImport Error: {e}")
print("Failed to import. Check paths and internal library structure.")
except Exception as e:
print(f"\nRuntime Error: {e}")
print("Imports might have worked, but usage failed.")