Repository: tefra/xsdata

Issue:
Support for mixed content for DTD elements?
It seems that if I specify mixed content for an element in a DTD, and generate a model from that DTD, the mixed content is not only ignored, but the dataclass is not generated. Example:

```dtd
<?xml version="1.0" encoding="UTF-8"?>

<!ELEMENT kap (#PCDATA|rad)*>

<!ELEMENT rad (#PCDATA)>
<!ATTLIST rad
       var CDATA #IMPLIED
>
```

Command line:

```sh
$ xsdata example.dtd
```

Result:

```py
from dataclasses import dataclass, field
from typing import Optional


@dataclass
class Rad:
    class Meta:
        name = "rad"

    value: str = field(
        default="",
        metadata={
            "required": True,
        }
    )
    var: Optional[str] = field(
        default=None,
        metadata={
            "type": "Attribute",
        }
    )
```

So the "kap" class was not generated.


Discussion:
sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!&nbsp; &nbsp; [![Quality Gate passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/passed-16px.png 'Quality Gate passed')](https://sonarcloud.io/dashboard?id=tefra_xsdata&pullRequest=749)

[![Bug](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug-16px.png 'Bug')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=749&resolved=false&types=BUG) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=749&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=749&resolved=false&types=BUG)  
[![Vulnerability](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability-16px.png 'Vulnerability')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=749&resolved=false&types=VULNERABILITY) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=749&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=749&resolved=false&types=VULNERABILITY)  
[![Security Hotspot](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot-16px.png 'Security Hotspot')](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=749&resolved=false&types=SECURITY_HOTSPOT) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=749&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=749&resolved=false&types=SECURITY_HOTSPOT)  
[![Code Smell](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell-16px.png 'Code Smell')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=749&resolved=false&types=CODE_SMELL) [![A](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A-16px.png 'A')](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=749&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=749&resolved=false&types=CODE_SMELL)

[![No Coverage information](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo-16px.png 'No Coverage information')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=749) No Coverage information  
[![0.0%](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3-16px.png '0.0%')](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=749&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=749&metric=new_duplicated_lines_density&view=list)


codecov[bot]: # [Codecov](https://codecov.io/gh/tefra/xsdata/pull/749?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) Report
Base: **100.00**% // Head: **99.98**% // Decreases project coverage by **`-0.02%`** :warning:
> Coverage data is based on head [(`cdc9762`)](https://codecov.io/gh/tefra/xsdata/pull/749?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) compared to base [(`8d431a2`)](https://codecov.io/gh/tefra/xsdata/commit/8d431a234d79c487950acc0f3eac019fb98d5088?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).
> Patch coverage: 91.66% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##            master     #749      +/-   ##
===========================================
- Coverage   100.00%   99.98%   -0.02%     
===========================================
  Files          101      101              
  Lines         8804     8814      +10     
  Branches      1953     1955       +2     
===========================================
+ Hits          8804     8813       +9     
- Partials         0        1       +1     
```


| [Impacted Files](https://codecov.io/gh/tefra/xsdata/pull/749?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou) | Coverage Δ | |
|---|---|---|
| [xsdata/codegen/mappers/dtd.py](https://codecov.io/gh/tefra/xsdata/pull/749?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou#diff-eHNkYXRhL2NvZGVnZW4vbWFwcGVycy9kdGQucHk=) | `99.26% <91.66%> (-0.74%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/tefra/xsdata/pull/749?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Chris+Tsou).


Patch:
diff --git a/xsdata/codegen/mappers/dtd.py b/xsdata/codegen/mappers/dtd.py
index 2cf4b9bf..fb7a4c91 100644
--- a/xsdata/codegen/mappers/dtd.py
+++ b/xsdata/codegen/mappers/dtd.py
@@ -21,6 +21,7 @@ from xsdata.models.dtd import DtdElement
 from xsdata.models.dtd import DtdElementType
 from xsdata.models.enums import DataType
 from xsdata.models.enums import Tag
+from xsdata.utils.constants import DEFAULT_ATTR_NAME
 
 
 class DtdMapper:
@@ -97,9 +98,9 @@ class DtdMapper:
         # "undefined", "empty", "any", "mixed", or "element";
         if element.type == DtdElementType.ELEMENT and element.content:
             cls.build_content(target, element.content)
-        elif element.type == DtdElementType.MIXED:
+        elif element.type == DtdElementType.MIXED and element.content:
             target.tag = Tag.COMPLEX_TYPE
-            cls.build_extension(target, DataType.STRING)
+            cls.build_content(target, element.content)
         elif element.type == DtdElementType.ANY:
             cls.build_extension(target, DataType.ANY_TYPE)
 
@@ -121,6 +122,9 @@ class DtdMapper:
             params = {"min_occurs": 0, "choice": str(id(content))}
             params.update(kwargs)
             cls.build_content_tree(target, content, **params)
+        else:  # content_type == DtdContentType.PCDATA:
+            restrictions = cls.build_restrictions(content.occur, **kwargs)
+            cls.build_value(target, restrictions)
 
     @classmethod
     def build_content_tree(cls, target: Class, content: DtdContent, **kwargs: Any):
@@ -162,6 +166,18 @@ class DtdMapper:
         attr.index = len(target.attrs)
         target.attrs.append(attr)
 
+    @classmethod
+    def build_value(cls, target: Class, restrictions: Restrictions):
+        types = AttrType(qname=str(DataType.STRING), native=True)
+        attr = Attr(
+            name=DEFAULT_ATTR_NAME,
+            tag=Tag.EXTENSION,
+            types=[types],
+            restrictions=restrictions.clone(),
+        )
+        attr.index = len(target.attrs)
+        target.attrs.append(attr)
+
     @classmethod
     def build_enumeration(cls, target: Class, name: str, values: List[str]):
         inner = Class(qname=name, tag=Tag.SIMPLE_TYPE, location=target.location)


Test patch:
diff --git a/tests/codegen/mappers/test_dtd.py b/tests/codegen/mappers/test_dtd.py
index acb96e9d..e5adf51a 100644
--- a/tests/codegen/mappers/test_dtd.py
+++ b/tests/codegen/mappers/test_dtd.py
@@ -151,15 +151,38 @@ class DtdMapperTests(FactoryTestCase):
 
     def test_build_elements_with_mixed_element_type(self):
         target = ClassFactory.create(tag=Tag.ELEMENT)
-        element = DtdElementFactory.create(type=DtdElementType.MIXED)
+        element = DtdElementFactory.create(
+            type=DtdElementType.MIXED,
+            content=DtdContentFactory.create(
+                type=DtdContentType.OR,
+                left=DtdContentFactory.create(
+                    type=DtdContentType.PCDATA, occur=DtdContentOccur.ONCE
+                ),
+                right=DtdContentFactory.create(
+                    name="rad", type=DtdContentType.ELEMENT, occur=DtdContentOccur.ONCE
+                ),
+            ),
+        )
 
         DtdMapper.build_elements(target, element)
-        self.assertEqual(0, len(target.attrs))
-        self.assertEqual(1, len(target.extensions))
-
+        expected = [
+            AttrFactory.create(
+                tag=Tag.EXTENSION,
+                name="value",
+                index=0,
+                types=[AttrTypeFactory.native(DataType.STRING)],
+            ),
+            AttrFactory.create(
+                tag=Tag.ELEMENT,
+                name="rad",
+                index=1,
+                types=[AttrTypeFactory.create(qname="rad")],
+            ),
+        ]
+
+        self.assertEqual(expected, target.attrs)
+        self.assertEqual(2, len(target.attrs))
         self.assertEqual(Tag.COMPLEX_TYPE, target.tag)
-        self.assertEqual(str(DataType.STRING), target.extensions[0].type.qname)
-        self.assertTrue(target.extensions[0].type.native)
 
     def test_build_elements_with_any_element_type(self):
         target = ClassFactory.create()
@@ -259,7 +282,16 @@ class DtdMapperTests(FactoryTestCase):
         )
         target = ClassFactory.create()
         DtdMapper.build_content(target, content)
-        self.assertEqual(0, len(target.attrs))
+
+        expected = [
+            AttrFactory.create(
+                tag=Tag.EXTENSION,
+                name="value",
+                index=0,
+                types=[AttrTypeFactory.native(DataType.STRING)],
+            )
+        ]
+        self.assertEqual(expected, target.attrs)
 
     @mock.patch.object(DtdMapper, "build_content")
     def test_build_content_tree(self, mock_build_content):