prompt: You will be provided with a partial code base and an issue statement explaining a problem to resolve.
  <issue>
  feat: Add split by `page` to `DocumentSplitter`
  **Is your feature request related to a problem? Please describe.**
  There are some cases where we would like to be able to split the contents of a PDF by page. Either to keep all text from a single page as a document to help preserve context or to be able to perform two sets of chunking (i.e. split by page, followed up by split by sentence). I would not say this is a common set up, but I believe we can straightforwardly extend the `DocumentSplitter` to have this flexibility. 
  
  **Describe the solution you'd like**
  Add a new `split_by` value of `page` that would split on `"\f"`.
  
  **Describe alternatives you've considered**
  Split up the source file (e.g. a PDF) into individual pages before feeding it into the Haystack pipeline. Definitely doable, but less elegant than having the `DocumentSplitter` being able to handle this.  
  
  
  </issue>
  <code>
  [start of haystack/components/preprocessors/document_splitter.py]
  1 from copy import deepcopy
  2 from typing import List, Literal
  3 
  4 from more_itertools import windowed
  5 
  6 from haystack import component, Document
  7 
  8 
  9 @component
  10 class DocumentSplitter:
  11     """
  12     Splits a list of text documents into a list of text documents with shorter texts.
  13     This is useful for splitting documents with long texts that otherwise would not fit into the maximum text length of language models.
  14     """
  15 
  16     def __init__(
  17         self, split_by: Literal["word", "sentence", "passage"] = "word", split_length: int = 200, split_overlap: int = 0
  18     ):
  19         """
  20         :param split_by: The unit by which the document should be split. Choose from "word" for splitting by " ",
  21         "sentence" for splitting by ".", or "passage" for splitting by "\\n\\n".
  22         :param split_length: The maximum number of units in each split.
  23         :param split_overlap: The number of units that each split should overlap.
  24         """
  25 
  26         self.split_by = split_by
  27         if split_by not in ["word", "sentence", "passage"]:
  28             raise ValueError("split_by must be one of 'word', 'sentence' or 'passage'.")
  29         if split_length <= 0:
  30             raise ValueError("split_length must be greater than 0.")
  31         self.split_length = split_length
  32         if split_overlap < 0:
  33             raise ValueError("split_overlap must be greater than or equal to 0.")
  34         self.split_overlap = split_overlap
  35 
  36     @component.output_types(documents=List[Document])
  37     def run(self, documents: List[Document]):
  38         """
  39         Splits the documents by split_by after split_length units with an overlap of split_overlap units.
  40         Returns a list of documents with the split texts.
  41         A metadata field "source_id" is added to each document to keep track of the original document that was split.
  42         Other metadata are copied from the original document.
  43         :param documents: The documents to split.
  44         :return: A list of documents with the split texts.
  45         """
  46 
  47         if not isinstance(documents, list) or (documents and not isinstance(documents[0], Document)):
  48             raise TypeError("DocumentSplitter expects a List of Documents as input.")
  49 
  50         split_docs = []
  51         for doc in documents:
  52             if doc.content is None:
  53                 raise ValueError(
  54                     f"DocumentSplitter only works with text documents but document.content for document ID {doc.id} is None."
  55                 )
  56             units = self._split_into_units(doc.content, self.split_by)
  57             text_splits = self._concatenate_units(units, self.split_length, self.split_overlap)
  58             metadata = deepcopy(doc.meta)
  59             metadata["source_id"] = doc.id
  60             split_docs += [Document(content=txt, meta=metadata) for txt in text_splits]
  61         return {"documents": split_docs}
  62 
  63     def _split_into_units(self, text: str, split_by: Literal["word", "sentence", "passage"]) -> List[str]:
  64         if split_by == "passage":
  65             split_at = "\n\n"
  66         elif split_by == "sentence":
  67             split_at = "."
  68         elif split_by == "word":
  69             split_at = " "
  70         else:
  71             raise NotImplementedError(
  72                 "DocumentSplitter only supports 'passage', 'sentence' or 'word' split_by options."
  73             )
  74         units = text.split(split_at)
  75         # Add the delimiter back to all units except the last one
  76         for i in range(len(units) - 1):
  77             units[i] += split_at
  78         return units
  79 
  80     def _concatenate_units(self, elements: List[str], split_length: int, split_overlap: int) -> List[str]:
  81         """
  82         Concatenates the elements into parts of split_length units.
  83         """
  84         text_splits = []
  85         segments = windowed(elements, n=split_length, step=split_length - split_overlap)
  86         for seg in segments:
  87             current_units = [unit for unit in seg if unit is not None]
  88             txt = "".join(current_units)
  89             if len(txt) > 0:
  90                 text_splits.append(txt)
  91         return text_splits
  92
  [end of haystack/components/preprocessors/document_splitter.py]
  </code>
  
  I need you to solve this issue by generating a single patch file that I can apply directly to this repository using git apply. Please respond with a single patch file in the following format.
  <patch>
  diff --git a/file.py b/file.py
  --- a/file.py
  +++ b/file.py
  @@ -1,27 +1,35 @@
   def euclidean(a, b):
  -    while b:
  -        a, b = b, a % b
  -    return a
  +    if b == 0:
  +        return a
  +    return euclidean(b, a % b)
   
   
   def bresenham(x0, y0, x1, y1):
       points = []
       dx = abs(x1 - x0)
       dy = abs(y1 - y0)
  -    sx = 1 if x0 < x1 else -1
  -    sy = 1 if y0 < y1 else -1
  -    err = dx - dy
  +    x, y = x0, y0
  +    sx = -1 if x0 > x1 else 1
  +    sy = -1 if y0 > y1 else 1
   
  -    while True:
  -        points.append((x0, y0))
  -        if x0 == x1 and y0 == y1:
  -            break
  -        e2 = 2 * err
  -        if e2 > -dy:
  -            err -= dy
  -            x0 += sx
  -        if e2 < dx:
  -            err += dx
  -            y0 += sy
  +    if dx > dy:
  +        err = dx / 2.0
  +        while x != x1:
  +            points.append((x, y))
  +            err -= dy
  +            if err < 0:
  +                y += sy
  +                err += dx
  +            x += sx
  +    else:
  +        err = dy / 2.0
  +        while y != y1:
  +            points.append((x, y))
  +            err -= dx
  +            if err < 0:
  +                x += sx
  +                err += dy
  +            y += sy
  +
  +    points.append((x, y))
       return points
  
  </patch>
golden_diff: diff --git a/haystack/components/preprocessors/document_splitter.py b/haystack/components/preprocessors/document_splitter.py
  --- a/haystack/components/preprocessors/document_splitter.py
  +++ b/haystack/components/preprocessors/document_splitter.py
  @@ -14,18 +14,21 @@
       """
   
       def __init__(
  -        self, split_by: Literal["word", "sentence", "passage"] = "word", split_length: int = 200, split_overlap: int = 0
  +        self,
  +        split_by: Literal["word", "sentence", "page", "passage"] = "word",
  +        split_length: int = 200,
  +        split_overlap: int = 0,
       ):
           """
           :param split_by: The unit by which the document should be split. Choose from "word" for splitting by " ",
  -        "sentence" for splitting by ".", or "passage" for splitting by "\\n\\n".
  +        "sentence" for splitting by ".", "page" for splitting by "\f" or "passage" for splitting by "\\n\\n".
           :param split_length: The maximum number of units in each split.
           :param split_overlap: The number of units that each split should overlap.
           """
   
           self.split_by = split_by
  -        if split_by not in ["word", "sentence", "passage"]:
  -            raise ValueError("split_by must be one of 'word', 'sentence' or 'passage'.")
  +        if split_by not in ["word", "sentence", "page", "passage"]:
  +            raise ValueError("split_by must be one of 'word', 'sentence', 'page' or 'passage'.")
           if split_length <= 0:
               raise ValueError("split_length must be greater than 0.")
           self.split_length = split_length
  @@ -60,8 +63,10 @@
               split_docs += [Document(content=txt, meta=metadata) for txt in text_splits]
           return {"documents": split_docs}
   
  -    def _split_into_units(self, text: str, split_by: Literal["word", "sentence", "passage"]) -> List[str]:
  -        if split_by == "passage":
  +    def _split_into_units(self, text: str, split_by: Literal["word", "sentence", "passage", "page"]) -> List[str]:
  +        if split_by == "page":
  +            split_at = "\f"
  +        elif split_by == "passage":
               split_at = "\n\n"
           elif split_by == "sentence":
               split_at = "."
  @@ -69,7 +74,7 @@
               split_at = " "
           else:
               raise NotImplementedError(
  -                "DocumentSplitter only supports 'passage', 'sentence' or 'word' split_by options."
  +                "DocumentSplitter only supports 'word', 'sentence', 'page' or 'passage' split_by options."
               )
           units = text.split(split_at)
           # Add the delimiter back to all units except the last one
  
verification_info: {"golden_diff": "diff --git a/haystack/components/preprocessors/document_splitter.py b/haystack/components/preprocessors/document_splitter.py\n--- a/haystack/components/preprocessors/document_splitter.py\n+++ b/haystack/components/preprocessors/document_splitter.py\n@@ -14,18 +14,21 @@\n     \"\"\"\n \n     def __init__(\n-        self, split_by: Literal[\"word\", \"sentence\", \"passage\"] = \"word\", split_length: int = 200, split_overlap: int = 0\n+        self,\n+        split_by: Literal[\"word\", \"sentence\", \"page\", \"passage\"] = \"word\",\n+        split_length: int = 200,\n+        split_overlap: int = 0,\n     ):\n         \"\"\"\n         :param split_by: The unit by which the document should be split. Choose from \"word\" for splitting by \" \",\n-        \"sentence\" for splitting by \".\", or \"passage\" for splitting by \"\\\\n\\\\n\".\n+        \"sentence\" for splitting by \".\", \"page\" for splitting by \"\\f\" or \"passage\" for splitting by \"\\\\n\\\\n\".\n         :param split_length: The maximum number of units in each split.\n         :param split_overlap: The number of units that each split should overlap.\n         \"\"\"\n \n         self.split_by = split_by\n-        if split_by not in [\"word\", \"sentence\", \"passage\"]:\n-            raise ValueError(\"split_by must be one of 'word', 'sentence' or 'passage'.\")\n+        if split_by not in [\"word\", \"sentence\", \"page\", \"passage\"]:\n+            raise ValueError(\"split_by must be one of 'word', 'sentence', 'page' or 'passage'.\")\n         if split_length <= 0:\n             raise ValueError(\"split_length must be greater than 0.\")\n         self.split_length = split_length\n@@ -60,8 +63,10 @@\n             split_docs += [Document(content=txt, meta=metadata) for txt in text_splits]\n         return {\"documents\": split_docs}\n \n-    def _split_into_units(self, text: str, split_by: Literal[\"word\", \"sentence\", \"passage\"]) -> List[str]:\n-        if split_by == \"passage\":\n+    def _split_into_units(self, text: str, split_by: Literal[\"word\", \"sentence\", \"passage\", \"page\"]) -> List[str]:\n+        if split_by == \"page\":\n+            split_at = \"\\f\"\n+        elif split_by == \"passage\":\n             split_at = \"\\n\\n\"\n         elif split_by == \"sentence\":\n             split_at = \".\"\n@@ -69,7 +74,7 @@\n             split_at = \" \"\n         else:\n             raise NotImplementedError(\n-                \"DocumentSplitter only supports 'passage', 'sentence' or 'word' split_by options.\"\n+                \"DocumentSplitter only supports 'word', 'sentence', 'page' or 'passage' split_by options.\"\n             )\n         units = text.split(split_at)\n         # Add the delimiter back to all units except the last one\n", "issue": "feat: Add split by `page` to `DocumentSplitter`\n**Is your feature request related to a problem? Please describe.**\r\nThere are some cases where we would like to be able to split the contents of a PDF by page. Either to keep all text from a single page as a document to help preserve context or to be able to perform two sets of chunking (i.e. split by page, followed up by split by sentence). I would not say this is a common set up, but I believe we can straightforwardly extend the `DocumentSplitter` to have this flexibility. \r\n\r\n**Describe the solution you'd like**\r\nAdd a new `split_by` value of `page` that would split on `\"\\f\"`.\r\n\r\n**Describe alternatives you've considered**\r\nSplit up the source file (e.g. a PDF) into individual pages before feeding it into the Haystack pipeline. Definitely doable, but less elegant than having the `DocumentSplitter` being able to handle this.  \r\n\n", "before_files": [{"content": "from copy import deepcopy\nfrom typing import List, Literal\n\nfrom more_itertools import windowed\n\nfrom haystack import component, Document\n\n\n@component\nclass DocumentSplitter:\n    \"\"\"\n    Splits a list of text documents into a list of text documents with shorter texts.\n    This is useful for splitting documents with long texts that otherwise would not fit into the maximum text length of language models.\n    \"\"\"\n\n    def __init__(\n        self, split_by: Literal[\"word\", \"sentence\", \"passage\"] = \"word\", split_length: int = 200, split_overlap: int = 0\n    ):\n        \"\"\"\n        :param split_by: The unit by which the document should be split. Choose from \"word\" for splitting by \" \",\n        \"sentence\" for splitting by \".\", or \"passage\" for splitting by \"\\\\n\\\\n\".\n        :param split_length: The maximum number of units in each split.\n        :param split_overlap: The number of units that each split should overlap.\n        \"\"\"\n\n        self.split_by = split_by\n        if split_by not in [\"word\", \"sentence\", \"passage\"]:\n            raise ValueError(\"split_by must be one of 'word', 'sentence' or 'passage'.\")\n        if split_length <= 0:\n            raise ValueError(\"split_length must be greater than 0.\")\n        self.split_length = split_length\n        if split_overlap < 0:\n            raise ValueError(\"split_overlap must be greater than or equal to 0.\")\n        self.split_overlap = split_overlap\n\n    @component.output_types(documents=List[Document])\n    def run(self, documents: List[Document]):\n        \"\"\"\n        Splits the documents by split_by after split_length units with an overlap of split_overlap units.\n        Returns a list of documents with the split texts.\n        A metadata field \"source_id\" is added to each document to keep track of the original document that was split.\n        Other metadata are copied from the original document.\n        :param documents: The documents to split.\n        :return: A list of documents with the split texts.\n        \"\"\"\n\n        if not isinstance(documents, list) or (documents and not isinstance(documents[0], Document)):\n            raise TypeError(\"DocumentSplitter expects a List of Documents as input.\")\n\n        split_docs = []\n        for doc in documents:\n            if doc.content is None:\n                raise ValueError(\n                    f\"DocumentSplitter only works with text documents but document.content for document ID {doc.id} is None.\"\n                )\n            units = self._split_into_units(doc.content, self.split_by)\n            text_splits = self._concatenate_units(units, self.split_length, self.split_overlap)\n            metadata = deepcopy(doc.meta)\n            metadata[\"source_id\"] = doc.id\n            split_docs += [Document(content=txt, meta=metadata) for txt in text_splits]\n        return {\"documents\": split_docs}\n\n    def _split_into_units(self, text: str, split_by: Literal[\"word\", \"sentence\", \"passage\"]) -> List[str]:\n        if split_by == \"passage\":\n            split_at = \"\\n\\n\"\n        elif split_by == \"sentence\":\n            split_at = \".\"\n        elif split_by == \"word\":\n            split_at = \" \"\n        else:\n            raise NotImplementedError(\n                \"DocumentSplitter only supports 'passage', 'sentence' or 'word' split_by options.\"\n            )\n        units = text.split(split_at)\n        # Add the delimiter back to all units except the last one\n        for i in range(len(units) - 1):\n            units[i] += split_at\n        return units\n\n    def _concatenate_units(self, elements: List[str], split_length: int, split_overlap: int) -> List[str]:\n        \"\"\"\n        Concatenates the elements into parts of split_length units.\n        \"\"\"\n        text_splits = []\n        segments = windowed(elements, n=split_length, step=split_length - split_overlap)\n        for seg in segments:\n            current_units = [unit for unit in seg if unit is not None]\n            txt = \"\".join(current_units)\n            if len(txt) > 0:\n                text_splits.append(txt)\n        return text_splits\n", "path": "haystack/components/preprocessors/document_splitter.py"}]}