Repository: tatuylonen/wiktextract

Issue:
[en] "Alternative forms" section data should be extracted to "forms" list
Wiktionary page: [harumph](https://en.wiktionary.org/wiki/harumph), [gung ho](https://en.wiktionary.org/wiki/gung_ho)
kaikki page: [harumph](https://kaikki.org/dictionary/All%20languages%20combined/meaning/h/ha/harumph.html), [gung ho](https://kaikki.org/dictionary/All%20languages%20combined/meaning/g/gu/gung%20ho.html)

Currently they are extracted to the "synonyms" list, and indistinguishable from data in "synonyms" section. It's much more easier to build a dictionary returns "gung ho" definition for word "gung-ho" if "Alternative forms" section data could be in "forms" list.

Hints from issue comments:
@kristian-clausal: There might be "Alternative forms" sections out there that do belong to "synonyms", but this seems like a sensible change.

Solution patch:
diff --git a/src/wiktextract/extractor/en/linkages.py b/src/wiktextract/extractor/en/linkages.py
index 0f71e3fc..b216540d 100644
--- a/src/wiktextract/extractor/en/linkages.py
+++ b/src/wiktextract/extractor/en/linkages.py
@@ -6,7 +6,10 @@ import re
 import unicodedata
 from typing import Optional, Sequence
 
-from ...datautils import data_append, split_at_comma_semi
+from wikitextprocessor import LevelNode, NodeKind, TemplateNode, WikiNode
+
+from ...datautils import data_append, data_extend, split_at_comma_semi
+from ...page import clean_node
 from ...tags import linkage_beginning_tags
 from ...wxr_context import WiktextractContext
 from .form_descriptions import (
@@ -20,7 +23,7 @@ from .form_descriptions import (
     parse_head_final_tags,
     parse_sense_qualifier,
 )
-from .type_utils import LinkageData, WordData
+from .type_utils import FormData, LinkageData, WordData
 
 # Linkage will be ignored if it matches this regexp before splitting
 linkage_pre_split_ignore_re = re.compile(
@@ -949,7 +952,7 @@ def parse_linkage_item_text(
                     url.strip() for url in urls if url and isinstance(url, str)
                 ]
             dt["word"] = w
-            for old in data.get(field, ()): # type: ignore[attr-defined]
+            for old in data.get(field, ()):  # type: ignore[attr-defined]
                 if dt == old:
                     break
             else:
@@ -1045,3 +1048,46 @@ def parse_linkage_item_text(
 
         add(item1, roman)
     return None
+
+
+def extract_alt_form_section(
+    wxr: WiktextractContext, word_entry: WordData, level_node: LevelNode
+) -> None:
+    for list_node in level_node.find_child(NodeKind.LIST):
+        for list_item in list_node.find_child(NodeKind.LIST_ITEM):
+            for node in list_item.children:
+                if isinstance(node, TemplateNode) and node.template_name in [
+                    "l",
+                    "link",
+                    "L",
+                    "alt",
+                    "alter",
+                ]:
+                    extract_l_template(wxr, word_entry, node)
+                elif isinstance(node, WikiNode) and node.kind == NodeKind.LINK:
+                    word = clean_node(wxr, None, node)
+                    if word != "":
+                        form: FormData = {"form": word, "tags": ["alternative"]}
+                        data_append(word_entry, "forms", form)
+
+
+def extract_l_template(
+    wxr: WiktextractContext, word_entry: WordData, t_node: TemplateNode
+) -> None:
+    forms: list[FormData] = []
+    expanded_node = wxr.wtp.parse(
+        wxr.wtp.node_to_wikitext(t_node), expand_all=True
+    )
+    lang_code = clean_node(wxr, None, t_node.template_parameters.get(1, ""))
+    for span_tag in expanded_node.find_html("span"):
+        span_lang = span_tag.attrs.get("lang", "")
+        if span_lang == lang_code:
+            word = clean_node(wxr, None, span_tag)
+            if word != "":
+                form: FormData = {"form": word, "tags": ["alternative"]}
+                forms.append(form)
+        elif span_lang.endswith("-Latn") and len(forms) > 0:
+            roman = clean_node(wxr, None, span_tag)
+            if roman != "":
+                forms[-1].roman = roman
+    data_extend(word_entry, "forms", forms)
diff --git a/src/wiktextract/extractor/en/page.py b/src/wiktextract/extractor/en/page.py
index 80ed33f8..1c8276f8 100644
--- a/src/wiktextract/extractor/en/page.py
+++ b/src/wiktextract/extractor/en/page.py
@@ -61,7 +61,7 @@ from .info_templates import (
     parse_info_template_arguments,
     parse_info_template_node,
 )
-from .linkages import parse_linkage_item_text
+from .linkages import parse_linkage_item_text, extract_alt_form_section
 from .parts_of_speech import PARTS_OF_SPEECH
 from .section_titles import (
     COMPOUNDS_TITLE,
@@ -3724,6 +3724,8 @@ def parse_language(
                 parse_translations(data, node)
             elif t in INFLECTION_TITLES:
                 parse_inflection(node, t, pos)
+            elif t == "alternative forms":
+                extract_alt_form_section(wxr, select_data(), node)
             else:
                 lst = t.split()
                 while len(lst) > 1 and lst[-1].isdigit():
@@ -4488,7 +4490,7 @@ def parse_page(wxr: WiktextractContext, word: str, text: str) -> list[WordData]:
                 )
             else:
                 wxr.wtp.debug(
-                    f"DIFFERENT ORIGINAL TITLE: '{word}' " f"-> '{x['word']}'",
+                    f"DIFFERENT ORIGINAL TITLE: '{word}' -> '{x['word']}'",
                     sortid="20231101/3582page.py",
                 )
             x["original_title"] = word
diff --git a/src/wiktextract/extractor/en/section_titles.py b/src/wiktextract/extractor/en/section_titles.py
index 05542a9a..853629a9 100644
--- a/src/wiktextract/extractor/en/section_titles.py
+++ b/src/wiktextract/extractor/en/section_titles.py
@@ -235,7 +235,6 @@ LINKAGE_TITLES: dict[str, str] = {
     "proverbs": "proverbs",
     "abbreviations": "abbreviations",
     "derived terms": "derived",
-    "alternative forms": "synonyms",
 }
 
 COMPOUNDS_TITLE = "compounds"


Test patch:
diff --git a/tests/test_en_alt_form_of.py b/tests/test_en_alt_form_of.py
index 4c337f9b..3ad9923d 100644
--- a/tests/test_en_alt_form_of.py
+++ b/tests/test_en_alt_form_of.py
@@ -10,13 +10,19 @@ from wiktextract.config import WiktionaryConfig
 from wiktextract.extractor.en.form_descriptions import (
     parse_alt_or_inflection_of,
 )
+from wiktextract.extractor.en.page import parse_page
 from wiktextract.thesaurus import close_thesaurus_db
 from wiktextract.wxr_context import WiktextractContext
 
 
 class FormOfTests(unittest.TestCase):
     def setUp(self):
-        self.wxr = WiktextractContext(Wtp(), WiktionaryConfig())
+        self.wxr = WiktextractContext(
+            Wtp(lang_code="en"),
+            WiktionaryConfig(
+                dump_file_lang_code="en", capture_language_codes=None
+            )
+        )
         self.wxr.wtp.start_page("testpage")
         self.wxr.wtp.start_section("English")
 
@@ -139,3 +145,26 @@ class FormOfTests(unittest.TestCase):
         self.assertEqual(
             ret, (["Western-Armenian", "alt-of"], [{"word": "OK"}])
         )
+
+    def test_alt_form_section(self):
+        self.wxr.wtp.add_page(
+            "Template:alter",
+            10,
+            """<span class="Latn" lang="scn">[[zùccuru#Sicilian|zùccuru]]</span>, <span class="Latn" lang="scn">[[zùcchiru#Sicilian|zùcchiru]]</span>""",
+        )
+        page_data = parse_page(
+            self.wxr,
+            "zùccaru",
+            """==Sicilian==
+===Alternative forms===
+* {{alter|scn|zùccuru|zùcchiru}}
+===Noun===
+# [[sugar]]""",
+        )
+        self.assertEqual(
+            page_data[0]["forms"],
+            [
+                {"form": "zùccuru", "tags": ["alternative"]},
+                {"form": "zùcchiru", "tags": ["alternative"]},
+            ],
+        )