kariminf/aruudy

View on GitHub
aruudy/poetry/prosody.py

Summary

Maintainability
A
0 mins
Test Coverage
#!/usr/bin/env python
# -*- coding: utf-8 -*-

#  Copyright 2016-2019 Abdelkrime Aries <kariminfo0@gmail.com>
#
#  ---- AUTHORS ----
#  2016-2019  Abdelkrime Aries <kariminfo0@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

"""
This module is used for prosody tasks:

- normalize a text
- find prosody form of a text
- find the meter

"""

import re
from aruudy.poetry import meter, foot
from aruudy.lists import change, const

def normalize(text):
    """Normalize a given text.

    The text is normalized by:

    - deleting non Arabic characters.
    - Deleting Tatweel (a character used as ligature )
    - Vocalizing and correcting letters

    Parameters
    ----------
    text : str
        The text to be normalized.

    Returns
    -------
    str
        A normalized text.

    """
    res = text #result

    # Filtering
    # ===========
    #delete tatweel
    res = re.sub(u"\u0640", u"", res)

    #delete any non wanted char
    res = re.sub(u"[^\u0621-\u0652\\s]", u"", res)

    # Tashkiil
    # ===========

    # allati التِي
    res = res = re.sub(u"(^|\\s)\u0627\u0644\u0651?\u064E?\u062A\u0650\u064A(\\s|$)", u"\\1\u0627\u0644\u0651\u064E\u062A\u0650\u064A\\2", res)

    # if fatha damma or kasra is before shadda: switch
    res = res = re.sub(u"([\u064B-\u0650])\u0651", u"\u0651\\1", res)

    # add Fatha to first al-
    res = re.sub(u"^\\s*\u0627\u0644", u"\u0627\u064E\u0644", res)

    # Falty fathatan on alif fix
    res = re.sub(u"([^\\s])\u064E?([\u0627\u0649])\u064B", u"\\1\u064B\\2", res)

    # if alif is preceeding waw: add sukuun to alif
    res = re.sub(u"\u0627\u0648", u"\u0627\u0652\u0648", res)

    # repeat 2 times when there are two consecutive alif, etc.
    for i in range(2):
        # add Fatha to any non diacretized char preceeding alif X 2
        res = re.sub(u"([^\u064B-\u0650\\s])([\u0627\u0649])([^\u064B-\u0652]|$)", u"\\1\u064E\\2\\3", res)

        #add Damma to any non diacretized char preceeding waw
        res = re.sub(u"([^\u064B-\u0652\\s])\u0648([^\u064B-\u0652]|$)", u"\\1\u064F\u0648\\2", res)

        #add Kasra to any non diacretized char preceeding yaa
        res = re.sub(u"([^\u064B-\u0652\\s])\u064A([^\u064B-\u0652]|$)", u"\\1\u0650\u064A\\2", res)

    # add Shadda to shamsi characters after al-
    res = re.sub(u"(^|\\s)\u0627(\u064E?)\u0644(%s)([^\u0651])" % const.SUN, u"\\1\u0627\\2\u0644\\3\u0651\\4", res)

    # add madda to other characters after al-
    res = re.sub(u"((?:^|\\s)\u0627\u0644[^\u0651])([^\u064E-\u0651])", u"\\1%s\\2" % const.UHARAKA, res)

    # add kasra to li
    res = re.sub(u"(^|\\s)\u0644([^\u064E-\u0652])", u"\\1\u0644\u0650\\2", res)

    # add kasra to bi
    res = re.sub(u"(^|\\s)\u0628([^\u064E-\u0652])", u"\\1\u0628\u0650\\2", res)

    # add fatha to fa
    res = re.sub(u"(^|\\s)\u0641([^\u064E-\u0652])", u"\\1\u0641\u064E\\2", res)

    # add fatha to wa
    res = re.sub(u"(^|\\s)\u0648([^\u064E-\u0652])", u"\\1\u0648\u064E\\2", res)

    # madda over alif with no vocalization
    res = re.sub(u"\u0623([^\u064B-\u0653\\s])", u"\u0623%s\\1" % const.UHARAKA, res)

    # hamza under alif with no kasra
    res = re.sub(u"\u0625([^\u0650])", u"\u0625\u0650\\1", res)

    #shadda not followed by a diacritic: add a madda above
    res = res = re.sub(u"\u0651([^\u064B-\u0650])", u"\u0651%s\\1" % const.UHARAKA, res)

    #add madda to any leading letter except alif
    res = res = re.sub(u"(^|\\s)([^\u0627])([^\u064E-\u0653])", u"\\1\\2%s\\3" % const.UHARAKA, res)

    #after sukuun must be a haraka
    res = res = re.sub(u"\u0652([^\\s])([^\u064B-\u0650\\s])", u"\u0652\\1%s\\2" % const.UHARAKA, res)

    return res

# https://ar.wikipedia.org/wiki/عروض

def _prosody_del(text):
    res = text
    # Replace al- with sun character (it can be preceded by prepositions bi- li-)
    # والصِّدق، والشَّمس ---> وصصِدق، وَششَمس
    res = re.sub(u"(%s)\u0627\u0644(%s)" % (const.DORJ, const.SUN) , u"\\1\\2", res)
    res = re.sub(u"\u0627\u064E\u0644(%s)" % const.SUN , u"\u0627\u064E\\1", res)

    # Replace al- with l otherwise
    # # والكتاب، فالعلم ---> وَلكتاب، فَلعِلم
    res = re.sub(u"(%s)\u0627(\u0644[^\u064E-\u0650])" % const.DORJ, u"\\1\\2", res)


    # delete first alif of a word in the middle of sentence
    # فاستمعَ، وافهم، واستماعٌ، وابنٌ، واثنان ---> فَستَمَعَ، وَفهَم، وَستِماعُن، وَبنُن، وَثنانِ
    res = re.sub(u"(%s)\u0627([^\\s][^\u064B-\u0651\u0653])" % const.DORJ , u"\\1\\2", res)

    # delete ending alif, waw and yaa preceeding a sakin
    # أتى المظلوم إلى القاضي فأنصفه قاضي العدل ---> أتَ لمظلوم إلَ لقاضي فأنصفه قاضِ لعدل.
    res = re.sub(const.ILLA + u"\\s+(.[^\u064B-\u0651\u0653])", u" \\1", res)

    # delete alif of plural masculin conjugation
    # رجعوا ---> رجعو
    res = re.sub(u"[\u064F]?\u0648\u0627(\\s+|$)", u"\u064F\u0648\u0652\\1", res)

    #TODO amruu
    # تحذف واو (عمرو) في الرفع والجر، مثل : حضر عَمرٌو، ذهبت إلى عَمرٍو، تكتب عروضيا هكذا : حضر عَمرُن، ذهبث إلى عَمرِن

    #تحذف الألف، والواو الزائدتين من : مائة، أنا، أولو، أولات، أولئك

    #تحذف الألف الأخيرة من الأدوات والحروف والأسماء الآتية إذا وليها ساكن : إذا، لماذا، هذا، كذا، إلا، ما، إذما، حاشا، خلا، عدا، كلا، لما

    return res

def _prosody_add(text):
    res = text

    #replace tanwiin taa marbuta by taa maftuuha
    res = re.sub(u"\u0629([\u064B-\u064D])", u"\u062A\\1", res)

    # delete alif from: fathatan + alif
    res = re.sub(u"\u064B(\u0627|\u0649)", u"\u064B", res)

    # Replace fathatan with fatha + nuun + sukuun
    res = re.sub(u"\u064B", u"\u064E\u0646\u0652", res)

    # Replace dammatun with damma + nuun + sukuun
    res = re.sub(u"\u064C", u"\u064F\u0646\u0652", res)

    # Replace kasratin with kasra + nuun + sukuun
    res = re.sub(u"\u064D", u"\u0650\u0646\u0652", res)

    # letter + Shadda ---> letter + sukuun + letter
    res = re.sub(u"(.)\u0651", u"\\1\u0652\\1", res)

    # hamza mamduuda --> alif-hamza + fatha + alif + sukuun
    res = re.sub(u"\u0622", u"\u0623\u064E\u0627\u0652", res)

    res = re.sub(u"([\u064E-\u0650])$", lambda m: const.TATWEEL[m.group(1)], res)

    return res

def _prosody_change(text):
    res = text
    res = re.sub(u"([^\s]+)", lambda m: change.modify(m.group(1)), res)
    return res

#TODO trait these
"""
تشبع حركة هاء الضمير الغائب للمفرد المذكر، وميم الجمع إن لم يترتب على ذلك كسر البيت الشعري، أو التقاء ساكنين، مثل : لهُ، بهِ، لكمُ، بكمُ، تكتب عروضيا هكذا : لهُو، بهِي، لكمُو، بكُمُو.
كاف المخاطب أو المخاطبة، ونون الرفع في الفعل المضارع، ونون جمع المذكر السالم، وتاء ضمير التكلم أو المخاطب للمذكر أو المؤنث تشبع حركتها إذا وقعت إحداها نهاية أحد الشطرين، مثل : كلامكَ، كلامُكِ، يسمعانِ، يسمعونَ، تسمعينَ، مسلمونَ، مسلمينَ، قُمتَ، قمتُ، قمتِ، تكتب عروضيا هكذا : كلامكَا، كلامكِي، يسمعانِي، يسمعونَا، تسمعينَا، مسلمونَا، مسلمينَا،، قُمتَا، قمتُو، قمتِي.
الهمزة الممدودة تكتب همزة مفتوحة بعدها ألف، مثل، آمن، قرآن، تكتب عروضيا هكذا: أَامَنَ، قرأَان.
الأحرف التي تحذف
"""


def prosody_form(text):
    """Generate the prosody form.

    The prosody form is generated by:

    - deleting some letters such as al-
    - adding some letters; for example, shadda.

    Parameters
    ----------
    text : str
        A normal vocalized Arabic text.

    Returns
    -------
    str
        The text in its prosody form.

    """
    res = text
    res = _prosody_change(res)
    res = _prosody_del(res)
    res = _prosody_add(res)
    return res


class Shatr(object):
    """Shatr (Hemistich) object.

    In Arabic, a verse is composed of two hemistiches (shatr).
    This object will contain all information about a given text.

    Parameters
    ----------
    text : str
        The text to be analysed.

    Attributes
    ----------
    orig : str
        the original input text.
    norm : str
        the text after normalization.
    prosody : str
        prosody form of a text.
    ameter : str
        the Arabic meter using "w" as watad (peg) and "s" as sabab (cord).
    emeter : str
        the English meter using "-" for long syllables and "u" for short ones.
    bahr : Bahr
        The bahr (meter) of the input text.
    parts : list(Part)
        The text split into different parts.
        Each part is a dict with these components:

        - emeter: the english meter of that part
        - type: the type of that part: is it regular or else
        - part: the text of that part
        - mnemonic: the mnemonic text of that part

    """
    def __init__(self, text):
        self.orig = text
        self.norm = normalize(text)
        self.prosody = prosody_form(self.norm)
        self.ameter, units = meter.get_ameter(self.prosody)
        self.emeter = meter.a2e_meter(self.ameter)
        self.bahr, self.parts = meter.search_bahr(self.emeter, units)

    def to_dict(self, bahr=False, parts=False):
        """Generates a dict object of the Bahr.

        Parameters
        ----------
        bahr : bool
            if true: the bahr object will be dictionarized too.
            By default, it is False. Hence, the "bahr" is an object of
            type Bahr
        parts : bool
            if true: the list of parts will be dictionarized too.
            By default, it is False. Hence, the parts in the list are objects of
            type Part

        Returns
        -------
        dict
            A dictionary object describing the Shatr.

            - norm (str): normalized text
            - prosody (str): prosody form of the text
            - ameter (str): Arabic meter of the text
            - emeter (str): English meter of the text
            - bahr (Bahr or dict): the bahr of the text
            - parts (list(Part or dict)): the parts of the text

        """

        res = {
            "norm": self.norm,
            "prosody": self.prosody,
            "ameter": self.ameter,
            "emeter": self.emeter,
            "bahr": self.bahr.to_dict() if self.bahr and bahr else self.bahr,
            "parts": self.parts
        }

        if self.parts and parts:
            res["parts"] = []
            for part in self.parts:
                res["parts"].append(part.to_dict())
        return res


class Bayt(object):
    def __init__(self, text, sep="\t"):
        self.original = text

def process_shatr(text):
    """process a shatr.

    Parameters
    ----------
    text : str
        The text representing the shatr (part of verse).

    Returns
    -------
    Shatr
        The Shatr containing information about the text.

    """
    return Shatr(text)