graingert: Use
 
`if urlparse.urlparse(base_url).scheme:`
peterbe: @graingert done. 
peterbe: @graingert I force-pushed a new commit message. Just for you :)
graingert: Bling
peterbe: So, @graingert you approve?

What do you think @ewjoachim ?
ewjoachim: Nothing to add, I'm ok.
diff --git a/premailer/premailer.py b/premailer/premailer.py
index b53fc23..2d3a221 100644
--- a/premailer/premailer.py
+++ b/premailer/premailer.py
@@ -17,7 +17,7 @@ if sys.version_info >= (3,):  # pragma: no cover
     # As in, Python 3
     from io import StringIO
     from urllib.request import urlopen
-    from urllib.parse import urljoin
+    from urllib.parse import urljoin, urlparse
     STR_TYPE = str
 else:  # Python 2
     try:
@@ -26,7 +26,7 @@ else:  # Python 2
         from StringIO import StringIO
         StringIO = StringIO  # shut up pyflakes
     from urllib2 import urlopen
-    from urlparse import urljoin
+    from urlparse import urljoin, urlparse
     STR_TYPE = basestring
 
 import cssutils
@@ -402,25 +402,23 @@ class Premailer(object):
         # URLs
         #
         if self.base_url:
-            if not self.base_url.endswith('/'):
-                self.base_url += '/'
+            if not urlparse(self.base_url).scheme:
+                raise ValueError('Base URL must start have a scheme')
             for attr in ('href', 'src'):
                 for item in page.xpath("//@%s" % attr):
                     parent = item.getparent()
+                    url = parent.attrib[attr]
                     if (
                         attr == 'href' and self.preserve_internal_links and
-                        parent.attrib[attr].startswith('#')
+                        url.startswith('#')
                     ):
                         continue
                     if (
                         attr == 'src' and self.preserve_inline_attachments and
-                        parent.attrib[attr].startswith('cid:')
+                        url.startswith('cid:')
                     ):
                         continue
-                    parent.attrib[attr] = urljoin(
-                        self.base_url,
-                        parent.attrib[attr].lstrip('/')
-                    )
+                    parent.attrib[attr] = urljoin(self.base_url, url)
 
         if hasattr(self.html, "getroottree"):
             return root

Replacement of urls beginning with //
When using a link whose href starts with "//", Premailer should just add http or https depending on the base url it uses. Currently, it replaces it with the full host, so ``//another.host/`` becomes ``http://my.base.host/another.host/``
diff --git a/premailer/tests/test_premailer.py b/premailer/tests/test_premailer.py
index ba63f85..4390a4e 100644
--- a/premailer/tests/test_premailer.py
+++ b/premailer/tests/test_premailer.py
@@ -478,7 +478,6 @@ class Tests(unittest.TestCase):
         </head>
         <body>
         <img src="/images/foo.jpg">
-        <img src="/images/bar.gif">
         <img src="http://www.googe.com/photos/foo.jpg">
         <a href="/home">Home</a>
         <a href="http://www.peterbe.com">External</a>
@@ -494,10 +493,9 @@ class Tests(unittest.TestCase):
         <title>Title</title>
         </head>
         <body>
-        <img src="http://kungfupeople.com/base/images/foo.jpg">
-        <img src="http://kungfupeople.com/base/images/bar.gif">
+        <img src="http://kungfupeople.com/images/foo.jpg">
         <img src="http://www.googe.com/photos/foo.jpg">
-        <a href="http://kungfupeople.com/base/home">Home</a>
+        <a href="http://kungfupeople.com/home">Home</a>
         <a href="http://www.peterbe.com">External</a>
         <a href="http://www.peterbe.com/base/">External 2</a>
         <a href="http://kungfupeople.com/base/subpage">Subpage</a>
@@ -505,7 +503,7 @@ class Tests(unittest.TestCase):
         </body>
         </html>'''
 
-        p = Premailer(html, base_url='http://kungfupeople.com/base',
+        p = Premailer(html, base_url='http://kungfupeople.com/base/',
                       preserve_internal_links=True)
         result_html = p.transform()
 
@@ -2302,3 +2300,36 @@ sheet" type="text/css">
         result_html = p.transform()
 
         compare_html(expect_html, result_html)
+
+    def test_links_without_protocol(self):
+        """If you the base URL is set to https://example.com and your html
+        contains <img src="//otherdomain.com/">... then the URL to point to
+        is "https://otherdomain.com/" not "https://example.com/file.css"
+        """
+        html = """<html>
+        <head>
+        </head>
+        <body>
+        <img src="//example.com">
+        </body>
+        </html>"""
+
+        expect_html = """<html>
+        <head>
+        </head>
+        <body>
+        <img src="{protocol}://example.com">
+        </body>
+        </html>"""
+
+        p = Premailer(html, base_url='https://www.peterbe.com')
+        result_html = p.transform()
+        compare_html(expect_html.format(protocol="https"), result_html)
+
+        p = Premailer(html, base_url='http://www.peterbe.com')
+        result_html = p.transform()
+        compare_html(expect_html.format(protocol="http"), result_html)
+
+        # Because you can't set a base_url without a full protocol
+        p = Premailer(html, base_url='www.peterbe.com')
+        assert_raises(ValueError, p.transform)

pip install -e .
parse_log_pytest
lxml cssselect cssutils nose mock
nose mock pytest
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
cssselect @ file:///croot/cssselect_1707339882883/work
cssutils @ file:///home/conda/feedstock_root/build_artifacts/cssutils_1734884840740/work
exceptiongroup==1.2.2
iniconfig==2.1.0
lxml @ file:///croot/lxml_1737039601731/work
mock @ file:///tmp/build/80754af9/mock_1607622725907/work
nose @ file:///opt/conda/conda-bld/nose_1642704612149/work
packaging==24.2
pluggy==1.5.0
-e git+https://github.com/peterbe/premailer.git@f211f3f1bcfc87dc0318f056d319bb5575f62a72#egg=premailer
pytest==8.3.5
tomli==2.2.1

name: premailer
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - cssselect=1.2.0=py39h06a4308_0
  - cssutils=2.11.0=pyhd8ed1ab_1
  - icu=73.1=h6a678d5_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - libxml2=2.13.5=hfdd30dd_0
  - libxslt=1.1.41=h097e994_0
  - lxml=5.3.0=py39h57af460_1
  - mock=4.0.3=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - nose=1.3.7=pyhd3eb1b0_1008
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - exceptiongroup==1.2.2
      - iniconfig==2.1.0
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - tomli==2.2.1
prefix: /opt/conda/envs/premailer

premailer/tests/test_premailer.py::Tests::test_base_url_with_path premailer/tests/test_premailer.py::Tests::test_links_without_protocol
premailer/tests/test_premailer.py::Tests::test_apple_newsletter_example premailer/tests/test_premailer.py::Tests::test_base_url_fixer premailer/tests/test_premailer.py::Tests::test_basic_html premailer/tests/test_premailer.py::Tests::test_basic_html_shortcut_function premailer/tests/test_premailer.py::Tests::test_basic_html_with_pseudo_selector premailer/tests/test_premailer.py::Tests::test_basic_xml premailer/tests/test_premailer.py::Tests::test_broken_xml premailer/tests/test_premailer.py::Tests::test_child_selector premailer/tests/test_premailer.py::Tests::test_command_line_fileinput_from_argument premailer/tests/test_premailer.py::Tests::test_command_line_fileinput_from_stdin premailer/tests/test_premailer.py::Tests::test_command_line_preserve_style_tags premailer/tests/test_premailer.py::Tests::test_comments_in_media_queries premailer/tests/test_premailer.py::Tests::test_css_disable_basic_html_attributes premailer/tests/test_premailer.py::Tests::test_css_text premailer/tests/test_premailer.py::Tests::test_css_text_with_only_body_present premailer/tests/test_premailer.py::Tests::test_css_with_html_attributes premailer/tests/test_premailer.py::Tests::test_css_with_pseudoclasses_excluded premailer/tests/test_premailer.py::Tests::test_css_with_pseudoclasses_included premailer/tests/test_premailer.py::Tests::test_disabled_validator premailer/tests/test_premailer.py::Tests::test_doctype premailer/tests/test_premailer.py::Tests::test_empty_style_tag premailer/tests/test_premailer.py::Tests::test_external_links premailer/tests/test_premailer.py::Tests::test_external_links_unfindable premailer/tests/test_premailer.py::Tests::test_external_styles_and_links premailer/tests/test_premailer.py::Tests::test_external_styles_on_http premailer/tests/test_premailer.py::Tests::test_external_styles_on_https premailer/tests/test_premailer.py::Tests::test_external_styles_with_base_url premailer/tests/test_premailer.py::Tests::test_favour_rule_with_class_over_generic premailer/tests/test_premailer.py::Tests::test_favour_rule_with_element_over_generic premailer/tests/test_premailer.py::Tests::test_favour_rule_with_id_over_others premailer/tests/test_premailer.py::Tests::test_favour_rule_with_important_over_others premailer/tests/test_premailer.py::Tests::test_fontface_selectors_with_no_selectortext premailer/tests/test_premailer.py::Tests::test_ignore_some_external_stylesheets premailer/tests/test_premailer.py::Tests::test_ignore_some_incorrectly premailer/tests/test_premailer.py::Tests::test_ignore_some_inline_stylesheets premailer/tests/test_premailer.py::Tests::test_ignore_style_elements_with_media_attribute premailer/tests/test_premailer.py::Tests::test_include_star_selector premailer/tests/test_premailer.py::Tests::test_inline_wins_over_external premailer/tests/test_premailer.py::Tests::test_keyframe_selectors premailer/tests/test_premailer.py::Tests::test_last_child premailer/tests/test_premailer.py::Tests::test_last_child_exclude_pseudo premailer/tests/test_premailer.py::Tests::test_leftover_important premailer/tests/test_premailer.py::Tests::test_load_external_url premailer/tests/test_premailer.py::Tests::test_load_external_url_gzip premailer/tests/test_premailer.py::Tests::test_mailto_url premailer/tests/test_premailer.py::Tests::test_mediaquery premailer/tests/test_premailer.py::Tests::test_merge_styles_basic premailer/tests/test_premailer.py::Tests::test_merge_styles_non_trivial premailer/tests/test_premailer.py::Tests::test_merge_styles_with_class premailer/tests/test_premailer.py::Tests::test_mixed_pseudo_selectors premailer/tests/test_premailer.py::Tests::test_multiple_style_elements premailer/tests/test_premailer.py::Tests::test_multithreading premailer/tests/test_premailer.py::Tests::test_parse_style_rules premailer/tests/test_premailer.py::Tests::test_precedence_comparison premailer/tests/test_premailer.py::Tests::test_prefer_inline_to_class premailer/tests/test_premailer.py::Tests::test_shortcut_function premailer/tests/test_premailer.py::Tests::test_strip_important premailer/tests/test_premailer.py::Tests::test_style_attribute_specificity premailer/tests/test_premailer.py::Tests::test_style_block_with_external_urls premailer/tests/test_premailer.py::Tests::test_turnoff_cache_works_as_expected premailer/tests/test_premailer.py::Tests::test_type_test premailer/tests/test_premailer.py::Tests::test_xml_cdata
premailer/premailer.py
premailer/premailer.py