diff --git a/routes/main_routes.py b/routes/main_routes.py
index 5cb3459..d347755 100644
--- a/routes/main_routes.py
+++ b/routes/main_routes.py
@@ -10,10 +10,19 @@
from utils.file_server import read_starter_code, resolve_starter_file, get_starter_code_dir
import os
+from utils.link_helper import parse_external_link
+
# Create the Blueprint that app.py will register
main = Blueprint("main", __name__)
+@main.app_template_filter("parse_resource")
+def parse_resource_filter(resource_str):
+ """Jinja filter to parse a resource string into a standardized dict."""
+ return parse_external_link(resource_str)
+
+
+
@main.route("/")
def index():
"""Render the homepage with the skill input form and dynamic stats."""
diff --git a/templates/project.html b/templates/project.html
index 85fa923..fe0a0fc 100644
--- a/templates/project.html
+++ b/templates/project.html
@@ -175,20 +175,14 @@
Learning Resources
{% for resource in project.resources %}
-
-
- {% set parts = resource.split(": http") %}
- {% if parts|length > 1 %}
-
- {{ parts[0] }}
-
-
+ {% set link_info = resource | parse_resource %}
+ {% if link_info.is_link %}
+
+ {{ link_info.label }}
+
+
{% else %}
- {{ resource }}
+ {{ link_info.label }}
{% endif %}
{% endfor %}
diff --git a/tests/test_basic.py b/tests/test_basic.py
index fc41aa7..01797a0 100644
--- a/tests/test_basic.py
+++ b/tests/test_basic.py
@@ -23,6 +23,7 @@
parse_skills,
score_single_project,
)
+from utils.link_helper import parse_external_link
from app import app, internal_server_error
@@ -73,6 +74,35 @@ def test_find_project_by_id_missing():
# Recommender utility tests
# ============================================================
+def test_parse_external_link():
+ """parse_external_link should correctly identify and split links and labels."""
+ assert parse_external_link("Python docs: https://docs.python.org") == {
+ "label": "Python docs",
+ "url": "https://docs.python.org",
+ "is_link": True
+ }
+ assert parse_external_link("Real Python - https://realpython.com") == {
+ "label": "Real Python",
+ "url": "https://realpython.com",
+ "is_link": True
+ }
+ assert parse_external_link("https://docs.python.org") == {
+ "label": "https://docs.python.org",
+ "url": "https://docs.python.org",
+ "is_link": True
+ }
+ assert parse_external_link("Some non-link text description") == {
+ "label": "Some non-link text description",
+ "url": None,
+ "is_link": False
+ }
+ assert parse_external_link("") == {
+ "label": "",
+ "url": None,
+ "is_link": False
+ }
+
+
def test_parse_skills_basic():
"""parse_skills should split on commas and lowercase each entry."""
result = parse_skills("Python, HTML, CSS")
diff --git a/utils/link_helper.py b/utils/link_helper.py
new file mode 100644
index 0000000..925aa39
--- /dev/null
+++ b/utils/link_helper.py
@@ -0,0 +1,47 @@
+# utils/link_helper.py
+# Reusable helper to parse and standardize external link handling.
+
+import re
+
+def parse_external_link(resource):
+ """
+ Safely parses a resource string which might contain an external URL.
+
+ Supports:
+ - "Label: https://url"
+ - "Label - https://url"
+ - "https://url" (plain URL)
+ - Plain text (no URL)
+
+ Returns a dict:
+ {
+ "label": str,
+ "url": str or None,
+ "is_link": bool
+ }
+ """
+ if not resource:
+ return {"label": "", "url": None, "is_link": False}
+
+ resource_str = str(resource).strip()
+
+ # Regex to find a standard http/https URL
+ url_match = re.search(r'https?://[^\s]+', resource_str)
+ if not url_match:
+ return {"label": resource_str, "url": None, "is_link": False}
+
+ url = url_match.group(0)
+
+ # The label is whatever is before the URL, stripped of colons, dashes, and spaces
+ label_part = resource_str[:url_match.start()].strip()
+ if label_part:
+ # Remove trailing colons, dashes, or spaces
+ label = re.sub(r'[:\-–—\s]+$', '', label_part).strip()
+ else:
+ label = url
+
+ return {
+ "label": label,
+ "url": url,
+ "is_link": True
+ }