View Issue Details

IDProjectCategoryView StatusLast Update
0014566ScribusStory Editor / Text Framespublic2017-12-20 20:58
ReporterFahad Assigned ToFahad  
PrioritynormalSeverityfeatureReproducibilityalways
Status closedResolutionfixed 
Product Version 
Target VersionFixed in Version1.5.4.svn 
Summary0014566: add Ignore diacritics & kashida option in Search\replace dialog
DescriptionIn arabic text, diacritics & kashida should be there is an option to ignore them similar to what Libreoffice has.
TagsNo tags attached.
PatchNo

Activities

Majda

2017-07-03 08:00

reporter   ~0044122

This is proposed solution to the problem stated above.

fixArabic.patch (7,774 bytes)
Index: Scribus/scribus/text/specialchars.cpp
===================================================================
--- Scribus/scribus/text/specialchars.cpp	(revision 22096)
+++ Scribus/scribus/text/specialchars.cpp	(working copy)
@@ -1,4 +1,4 @@
-/*
+/*
  For general Scribus (>=1.3.2) copyright and licensing information please refer
  to the COPYING file provided with the program. Following this notice may exist
  a copyright and/or license notice that predates the release of Scribus 1.3.2
@@ -261,3 +261,18 @@
 	else
 		return false;
 }
+
+bool SpecialChars::isArabicDiacritics(uint ch)
+{
+	if (ch >= 0x064B && ch <= 0x065F) /*Fathatan, Dammatan, Kasratan, Fatha,Damma,Kasra,Shadda,Sukun,Maddah, Hamza Above, Hamza Below,Subscript Alef,Inverted Damma,Mark Noon Ghunna*/
+		return true;
+	else if (ch == 0x0640) //Tatweel
+		return true;
+	else if (ch >= 0x06D2 && ch <= 0x06DC) /*Yeh Barree,Yeh Barree With Hamza Above, Full Stop Ae,Sad With Lam With Alef,Qaf With Lam With Alef,Small High Meem Initial Form, Small High Lam Alef, Small High Jeem , Small High Three Dots, Small High Seen */
+		return true;
+	else if (ch >= 0x06DF && ch <= 0x06E8) /*Small High Rounded Zero, Upright Rectangular Zero,Dotless Head Of Khah,Meem Isolated Form, Small Low Seen, Small High Madda,Small Waw, Small Yeh, Small High Yeh, Small High Noon */
+		return true;
+	else if (ch >= 0x0618 && ch <= 0x061A) /*Small Fatha, Small Damma, Small Kasra*/
+		return true;
+	return false;
+}
Index: Scribus/scribus/text/specialchars.h
===================================================================
--- Scribus/scribus/text/specialchars.h	(revision 22096)
+++ Scribus/scribus/text/specialchars.h	(working copy)
@@ -80,6 +80,7 @@
 	static bool isCJK(uint ch);
 	static bool isLetterRequiringSpaceAroundCJK(uint ch);
 	static bool isIgnorableCodePoint(uint ch);
+	static bool isArabicDiacritics(uint ch);
 };
 
 #endif
Index: Scribus/scribus/text/storytext.cpp
===================================================================
--- Scribus/scribus/text/storytext.cpp	(revision 22096)
+++ Scribus/scribus/text/storytext.cpp	(working copy)
@@ -352,9 +352,15 @@
 		while (i >= 0 && i < (int) d->len)
 		{
 			int index = 0;
-			while ((index < strLen) && ((index + i) < storyLen))
+			int ArDiacriticsCounter = 0; //counter for arabic diacritics
+			while ((index < strLen) && ((index + i + ArDiacriticsCounter ) < storyLen))
 			{
-				if (qStr.at(index) != d->at(index + i)->ch.toLower())
+				if (SpecialChars::isArabicDiacritics(d->at(index + ArDiacriticsCounter + i)->ch.unicode()))
+				{
+					++ArDiacriticsCounter;
+					continue;
+				}
+				if (qStr.at(index) != d->at(index + ArDiacriticsCounter + i)->ch.toLower())
 					break;
 				++index;
 			}
@@ -366,7 +372,6 @@
 			i = indexOf(ch, i + 1, cs);
 		}
 	}
-
 	return foundIndex;
 }
 
Index: Scribus/scribus/ui/search.cpp
===================================================================
--- Scribus/scribus/ui/search.cpp	(revision 22096)
+++ Scribus/scribus/ui/search.cpp	(working copy)
@@ -261,7 +261,7 @@
 	if (mode)
 		Word->setEnabled(false);
 	OptsLayout->addWidget( Word );
-	CaseIgnore = new QCheckBox( tr( "&Ignore Case" ), this );
+	CaseIgnore = new QCheckBox( tr( "&Ignore Case/Diacritics/Kashida" ), this );
 	if (mode)
 		CaseIgnore->setEnabled(false);
 	OptsLayout->addWidget( CaseIgnore );
@@ -508,15 +508,43 @@
 			}
 			if (found && searchForReplace)
 			{
-				m_item->itemText.select(a, sText.length());
-				m_item->HasSel = true;
-				if (rep)
+				if (cs == Qt::CaseSensitive)
 				{
-					DoReplace->setEnabled(true);
-					AllReplace->setEnabled(true);
+					m_item->itemText.select(a, sText.length());
+					m_item->HasSel = true;
+					if (rep)
+					{
+						DoReplace->setEnabled(true);
+						AllReplace->setEnabled(true);
+					}
+					m_item->itemText.setCursorPosition(a + sText.length());
+
+					if (!SText->isChecked())
+						break;
+
+					m_replStart = a;
+					break;
 				}
-				m_item->itemText.setCursorPosition(a + sText.length());
-
+				else
+				{
+					//Case insensitive calculate the length of the word in the text with arabic diacritics
+					int textLen = 0;
+					for (int j = 0 ; j < sText.length(); j++)
+					{
+						if (!SpecialChars::isArabicDiacritics(m_item->itemText.plainText().at(a + textLen).unicode()))
+							++textLen;
+						while(m_item->itemText.plainText().length() > (a + textLen) && SpecialChars::isArabicDiacritics(m_item->itemText.plainText().at(a + textLen).unicode()))
+							++textLen;
+					}
+					m_item->itemText.select(a, textLen);
+					m_item->HasSel = true;
+					if (rep)
+					{
+						DoReplace->setEnabled(true);
+						AllReplace->setEnabled(true);
+					}
+					m_item->itemText.setCursorPosition(a + textLen);
+				}
 				if (!SText->isChecked())
 					break;
 
@@ -685,36 +713,71 @@
 	{
 		QString repl, sear;
 		int cs, cx;
-// 		ScText *hg;
+		int textLen = 0;
 		if (RText->isChecked())
 		{
 			repl = RTextVal->text();
 			sear = STextVal->text();
-			if (sear.length() == repl.length())
+			if (CaseIgnore->isChecked())
 			{
-				for (cs = 0; cs < sear.length(); ++cs)
-					m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
+				//case insensitive calculate word length with diacritics
+				for (int j = 0 ; j < sear.length(); j++)
+				{
+					if (!SpecialChars::isArabicDiacritics(m_item->itemText.plainText().at(m_replStart + textLen).unicode()))
+						++textLen;
+					while((unsigned)m_item->itemText.plainText().length() > (m_replStart + textLen) && SpecialChars::isArabicDiacritics(m_item->itemText.plainText().at(m_replStart + textLen).unicode()))
+						++textLen;
+				}
+				if (textLen == repl.length())
+				{
+					for (cs = 0; cs < textLen; ++cs)
+						m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
+				}
+				else
+				{
+					if (textLen < repl.length())
+					{
+						for (cs = 0; cs < textLen; ++cs)
+							m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
+						for (cx = cs; cx < repl.length(); ++cx)
+							m_item->itemText.insertChars(m_replStart+cx, repl.mid(cx,1), true);
+					}
+					else
+					{
+						for (cs = 0; cs < repl.length(); ++cs)
+							m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
+						m_item->itemText.removeChars(m_replStart+cs, textLen - cs);
+					}
+				}
 			}
 			else
 			{
-				if (sear.length() < repl.length())
+				//case sensitive the replaced word has the same number of characters as the word of the text
+				if (sear.length() == repl.length())
 				{
 					for (cs = 0; cs < sear.length(); ++cs)
 						m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
-					for (cx = cs; cx < repl.length(); ++cx)
-						m_item->itemText.insertChars(m_replStart+cx, repl.mid(cx,1), true);
-					// FIXME:NLS also replace styles!!
 				}
 				else
 				{
-					for (cs = 0; cs < repl.length(); ++cs)
-						m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
-					m_item->itemText.removeChars(m_replStart+cs, sear.length() - cs);
+					if (sear.length() < repl.length())
+					{
+						for (cs = 0; cs < sear.length(); ++cs)
+							m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
+						for (cx = cs; cx < repl.length(); ++cx)
+							m_item->itemText.insertChars(m_replStart+cx, repl.mid(cx,1), true);
+					}
+					else
+					{
+						for (cs = 0; cs < repl.length(); ++cs)
+							m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
+						m_item->itemText.removeChars(m_replStart+cs, sear.length() - cs);
+					}
 				}
-				m_item->itemText.deselectAll();
-				if (repl.length() > 0)
-					m_item->itemText.select(m_replStart, repl.length());
 			}
+			m_item->itemText.deselectAll();
+			if (repl.length() > 0)
+				m_item->itemText.select(m_replStart, repl.length());
 		}
 		if (RStyle->isChecked())
 		{
fixArabic.patch (7,774 bytes)

Fahad

2017-07-06 08:48

developer   ~0044129

any chance to review the patch and merge it?

cbradney

2017-07-06 20:09

administrator   ~0044131

I've committed the first part of the code which is non-intrusive. I'm not sure if the code in storytext.cpp is going to slow text down too much. I realise we might require the code but I want Jean to review too.
I also renamed isArabicDiacritics to isArabicDiacritic

Fahad

2017-07-07 04:38

developer   ~0044133

@cbradney Thanks. I appreciate your effort.

Majda

2017-07-09 09:31

reporter   ~0044152

@cbradney Thank you for viewing the code. Regarding your concern about slowing down storytext.cpp, I would like to clarify that the function I modified which is IndexOf function in storytext.cpp is only used in the search process so can you explain how will it slow the text down?
Secondly, I attached another patch which contains an improvement in search.cpp if you can please check it out.

arabicfix.patch (4,416 bytes)
Index: Scribus/scribus/text/storytext.cpp
===================================================================
--- Scribus/scribus/text/storytext.cpp	(revision 22104)
+++ Scribus/scribus/text/storytext.cpp	(working copy)
@@ -352,9 +352,15 @@
 		while (i >= 0 && i < (int) d->len)
 		{
 			int index = 0;
-			while ((index < strLen) && ((index + i) < storyLen))
+			int ArDiacriticsCounter = 0; //counter for arabic diacritics
+			while ((index < strLen) && ((index + i + ArDiacriticsCounter ) < storyLen))
 			{
-				if (qStr.at(index) != d->at(index + i)->ch.toLower())
+				if (SpecialChars::isArabicDiacritic(d->at(index + ArDiacriticsCounter + i)->ch.unicode()))
+				{
+					++ArDiacriticsCounter;
+					continue;
+				}
+				if (qStr.at(index) != d->at(index + ArDiacriticsCounter + i)->ch.toLower())
 					break;
 				++index;
 			}
@@ -366,7 +372,6 @@
 			i = indexOf(ch, i + 1, cs);
 		}
 	}
-
 	return foundIndex;
 }
 
Index: Scribus/scribus/ui/search.cpp
===================================================================
--- Scribus/scribus/ui/search.cpp	(revision 22104)
+++ Scribus/scribus/ui/search.cpp	(working copy)
@@ -261,7 +261,7 @@
 	if (mode)
 		Word->setEnabled(false);
 	OptsLayout->addWidget( Word );
-	CaseIgnore = new QCheckBox( tr( "&Ignore Case" ), this );
+	CaseIgnore = new QCheckBox( tr( "&Ignore Case/Diacritics/Kashida" ), this );
 	if (mode)
 		CaseIgnore->setEnabled(false);
 	OptsLayout->addWidget( CaseIgnore );
@@ -508,7 +508,21 @@
 			}
 			if (found && searchForReplace)
 			{
-				m_item->itemText.select(a, sText.length());
+				int textLen = 0;
+				if (cs == Qt::CaseSensitive)
+					textLen = sText.length();
+				else
+				{
+					//Case insensitive calculate the length of the word in the text with arabic diacritics
+					for (int j = 0 ; j < sText.length(); j++)
+					{
+						if (!SpecialChars::isArabicDiacritic(m_item->itemText.plainText().at(a + textLen).unicode()))
+							++textLen;
+						while(m_item->itemText.plainText().length() > (a + textLen) && SpecialChars::isArabicDiacritic(m_item->itemText.plainText().at(a + textLen).unicode()))
+							++textLen;
+					}
+				}
+				m_item->itemText.select(a, textLen);
 				m_item->HasSel = true;
 				if (rep)
 				{
@@ -515,7 +529,7 @@
 					DoReplace->setEnabled(true);
 					AllReplace->setEnabled(true);
 				}
-				m_item->itemText.setCursorPosition(a + sText.length());
+				m_item->itemText.setCursorPosition(a + textLen);
 
 				if (!SText->isChecked())
 					break;
@@ -685,36 +699,48 @@
 	{
 		QString repl, sear;
 		int cs, cx;
-// 		ScText *hg;
+		int textLen = 0;
 		if (RText->isChecked())
 		{
 			repl = RTextVal->text();
 			sear = STextVal->text();
-			if (sear.length() == repl.length())
+			if (CaseIgnore->isChecked())
 			{
-				for (cs = 0; cs < sear.length(); ++cs)
+				//case insensitive calculate word length with diacritics
+				for (int j = 0 ; j < sear.length(); j++)
+				{
+					if (!SpecialChars::isArabicDiacritic(m_item->itemText.plainText().at(m_replStart + textLen).unicode()))
+						++textLen;
+					while((unsigned)m_item->itemText.plainText().length() > (m_replStart + textLen) && SpecialChars::isArabicDiacritic(m_item->itemText.plainText().at(m_replStart + textLen).unicode()))
+						++textLen;
+				}
+			}
+			else
+				textLen = sear.length();
+			if (textLen == repl.length())
+			{
+				for (cs = 0; cs < textLen; ++cs)
 					m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
 			}
 			else
 			{
-				if (sear.length() < repl.length())
+				if (textLen < repl.length())
 				{
-					for (cs = 0; cs < sear.length(); ++cs)
+					for (cs = 0; cs < textLen; ++cs)
 						m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
 					for (cx = cs; cx < repl.length(); ++cx)
 						m_item->itemText.insertChars(m_replStart+cx, repl.mid(cx,1), true);
-					// FIXME:NLS also replace styles!!
 				}
 				else
 				{
 					for (cs = 0; cs < repl.length(); ++cs)
 						m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
-					m_item->itemText.removeChars(m_replStart+cs, sear.length() - cs);
+					m_item->itemText.removeChars(m_replStart+cs, textLen - cs);
 				}
-				m_item->itemText.deselectAll();
-				if (repl.length() > 0)
-					m_item->itemText.select(m_replStart, repl.length());
 			}
+			m_item->itemText.deselectAll();
+			if (repl.length() > 0)
+				m_item->itemText.select(m_replStart, repl.length());
 		}
 		if (RStyle->isChecked())
 		{
arabicfix.patch (4,416 bytes)

jghali

2017-07-09 13:18

administrator   ~0044153

@Majda I add a look at your patch. One thing obvious: you do not want to use StoryText::plainText() where you used it in search.cpp. If you look at plainText() implementation, you would see it would make search/replace very slow for long text. I uploaded a modified patch.

14566_ignore_diacritics.patch (4,285 bytes)
Index: scribus/text/storytext.cpp
===================================================================
--- scribus/text/storytext.cpp	(revision 22102)
+++ scribus/text/storytext.cpp	(working copy)
@@ -352,9 +352,15 @@
 		while (i >= 0 && i < (int) d->len)
 		{
 			int index = 0;
-			while ((index < strLen) && ((index + i) < storyLen))
+			int arDiacriticsCounter = 0; //counter for arabic diacritics
+			while ((index < strLen) && ((index + i + arDiacriticsCounter ) < storyLen))
 			{
-				if (qStr.at(index) != d->at(index + i)->ch.toLower())
+				if (SpecialChars::isArabicDiacritic(d->at(index + arDiacriticsCounter + i)->ch.unicode()))
+				{
+					++arDiacriticsCounter;
+					continue;
+				}
+				if (qStr.at(index) != d->at(index + arDiacriticsCounter + i)->ch.toLower())
 					break;
 				++index;
 			}
@@ -366,7 +372,6 @@
 			i = indexOf(ch, i + 1, cs);
 		}
 	}
-
 	return foundIndex;
 }
 
Index: scribus/ui/search.cpp
===================================================================
--- scribus/ui/search.cpp	(revision 22102)
+++ scribus/ui/search.cpp	(working copy)
@@ -261,7 +261,7 @@
 	if (mode)
 		Word->setEnabled(false);
 	OptsLayout->addWidget( Word );
-	CaseIgnore = new QCheckBox( tr( "&Ignore Case" ), this );
+	CaseIgnore = new QCheckBox( tr( "&Ignore Case/Diacritics/Kashida" ), this );
 	if (mode)
 		CaseIgnore->setEnabled(false);
 	OptsLayout->addWidget( CaseIgnore );
@@ -508,14 +508,28 @@
 			}
 			if (found && searchForReplace)
 			{
-				m_item->itemText.select(a, sText.length());
+				int textLen = 0;
+				if (cs == Qt::CaseSensitive)
+					textLen = sText.length();
+				else
+				{
+					//Case insensitive calculate the length of the word in the text with arabic diacritics
+					for (int j = 0 ; j < sText.length(); j++)
+					{
+						if (!SpecialChars::isArabicDiacritic(m_item->itemText.text(a + textLen).unicode()))
+							++textLen;
+						while(m_item->itemText.length() > (a + textLen) && SpecialChars::isArabicDiacritic(m_item->itemText.text(a + textLen).unicode()))
+							++textLen;
+					}
+				}
+				m_item->itemText.select(a, textLen);
 				m_item->HasSel = true;
 				if (rep)
 				{
 					DoReplace->setEnabled(true);
 					AllReplace->setEnabled(true);
 				}
-				m_item->itemText.setCursorPosition(a + sText.length());
+				m_item->itemText.setCursorPosition(a + textLen);
 
 				if (!SText->isChecked())
 					break;
@@ -685,36 +699,48 @@
 	{
 		QString repl, sear;
 		int cs, cx;
-// 		ScText *hg;
+		int textLen = 0;
 		if (RText->isChecked())
 		{
 			repl = RTextVal->text();
 			sear = STextVal->text();
-			if (sear.length() == repl.length())
+			if (CaseIgnore->isChecked())
 			{
-				for (cs = 0; cs < sear.length(); ++cs)
+				//case insensitive calculate word length with diacritics
+				for (int j = 0 ; j < sear.length(); j++)
+				{
+					if (!SpecialChars::isArabicDiacritic(m_item->itemText.text(m_replStart + textLen).unicode()))
+						++textLen;
+					while((unsigned)m_item->itemText.length() > (m_replStart + textLen) && SpecialChars::isArabicDiacritic(m_item->itemText.text(m_replStart + textLen).unicode()))
+						++textLen;
+				}
+			}
+			else
+				textLen = sear.length();
+			if (textLen == repl.length())
+			{
+				for (cs = 0; cs < textLen; ++cs)
 					m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
 			}
 			else
 			{
-				if (sear.length() < repl.length())
+				if (textLen < repl.length())
 				{
-					for (cs = 0; cs < sear.length(); ++cs)
+					for (cs = 0; cs < textLen; ++cs)
 						m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
 					for (cx = cs; cx < repl.length(); ++cx)
 						m_item->itemText.insertChars(m_replStart+cx, repl.mid(cx,1), true);
-					// FIXME:NLS also replace styles!!
 				}
 				else
 				{
 					for (cs = 0; cs < repl.length(); ++cs)
 						m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
-					m_item->itemText.removeChars(m_replStart+cs, sear.length() - cs);
+					m_item->itemText.removeChars(m_replStart+cs, textLen - cs);
 				}
-				m_item->itemText.deselectAll();
-				if (repl.length() > 0)
-					m_item->itemText.select(m_replStart, repl.length());
 			}
+			m_item->itemText.deselectAll();
+			if (repl.length() > 0)
+				m_item->itemText.select(m_replStart, repl.length());
 		}
 		if (RStyle->isChecked())
 		{

jghali

2017-07-09 14:10

administrator   ~0044154

I'm also thinking about something: arabic diacritics are apparently part of the unicode "mark, non spacing" category (are kashidas part of this category too?):
http://www.fileformat.info/info/unicode/category/Mn/list.htm

This category contains diacritics for other languages. I'm not sure those have the same role than in arabic, but I wonder if there is some potential of extension of that patch for other languages.

Fahad

2017-07-09 16:11

developer   ~0044155

kashida, which known as 'ARABIC TATWEEL' (U+0640), belongs to 'Letter, Modifier' Category.

>>This category contains diacritics for other languages. I'm not sure those have the same role than in arabic, but I wonder if there is some potential of extension of that patch >>for other languages.

The problem here is who is going to test it?

jghali

2017-07-10 19:53

administrator   ~0044158

Yep, this is a problem. Anyway, does my modified patch still works as expected?

A last change I will probably do is modifying the label of the checkbox. As is, it would look like we ignore all diacritics, which is not the case. So I would use something like "Ignore Case and Arabic Diacritics/Kashida".

Majda

2017-07-11 07:54

reporter   ~0044163

@Jghali Thanks for your reply, yes the modified patch still works.
I have fixed diacritics using ICU NON-SPACING mark and tested it for Arabic and Hebrew. I have also added Arabic modifier letter for kashida.
The patch is attached below if you can please check it out.

Majda

2017-07-11 07:55

reporter  

fixArabic-2.patch (7,115 bytes)
Index: trunk/Scribus/scribus/text/specialchars.cpp
===================================================================
--- trunk/Scribus/scribus/text/specialchars.cpp	(revision 22104)
+++ trunk/Scribus/scribus/text/specialchars.cpp	(working copy)
@@ -262,17 +262,13 @@
 		return false;
 }
 
-bool SpecialChars::isArabicDiacritic(uint ch)
+bool SpecialChars::isArabicModifierLetter(uint ch)
 {
-	if (ch >= 0x064B && ch <= 0x065F) /*Fathatan, Dammatan, Kasratan, Fatha,Damma,Kasra,Shadda,Sukun,Maddah, Hamza Above, Hamza Below,Subscript Alef,Inverted Damma,Mark Noon Ghunna*/
+	if (ch == 0x0640) //ARABIC TATWEEL
 		return true;
-	else if (ch == 0x0640) //Tatweel
+	else if (ch == 0x06E5) //ARABIC SMALL WAW
 		return true;
-	else if (ch >= 0x06D2 && ch <= 0x06DC) /*Yeh Barree,Yeh Barree With Hamza Above, Full Stop Ae,Sad With Lam With Alef,Qaf With Lam With Alef,Small High Meem Initial Form, Small High Lam Alef, Small High Jeem , Small High Three Dots, Small High Seen */
+	else if (ch == 0x06E6) //ARABIC SMALL YEH
 		return true;
-	else if (ch >= 0x06DF && ch <= 0x06E8) /*Small High Rounded Zero, Upright Rectangular Zero,Dotless Head Of Khah,Meem Isolated Form, Small Low Seen, Small High Madda,Small Waw, Small Yeh, Small High Yeh, Small High Noon */
-		return true;
-	else if (ch >= 0x0618 && ch <= 0x061A) /*Small Fatha, Small Damma, Small Kasra*/
-		return true;
 	return false;
 }
Index: trunk/Scribus/scribus/text/specialchars.h
===================================================================
--- trunk/Scribus/scribus/text/specialchars.h	(revision 22104)
+++ trunk/Scribus/scribus/text/specialchars.h	(working copy)
@@ -80,7 +80,7 @@
 	static bool isCJK(uint ch);
 	static bool isLetterRequiringSpaceAroundCJK(uint ch);
 	static bool isIgnorableCodePoint(uint ch);
-	static bool isArabicDiacritic(uint ch);
+	static bool isArabicModifierLetter(uint ch);
 };
 
 #endif
Index: trunk/Scribus/scribus/text/storytext.cpp
===================================================================
--- trunk/Scribus/scribus/text/storytext.cpp	(revision 22104)
+++ trunk/Scribus/scribus/text/storytext.cpp	(working copy)
@@ -352,9 +352,16 @@
 		while (i >= 0 && i < (int) d->len)
 		{
 			int index = 0;
-			while ((index < strLen) && ((index + i) < storyLen))
+			int DiacriticsCounter = 0; //counter for diacritics
+			while ((index < strLen) && ((index + i + DiacriticsCounter ) < storyLen))
 			{
-				if (qStr.at(index) != d->at(index + i)->ch.toLower())
+				if (SpecialChars::isArabicModifierLetter(d->at(index + DiacriticsCounter + i)->ch.unicode()) ||
+						u_charType((d->at(index + DiacriticsCounter + i)->ch.unicode())) == UCharCategory::U_NON_SPACING_MARK)
+				{
+					++DiacriticsCounter;
+					continue;
+				}
+				if (qStr.at(index) != d->at(index + DiacriticsCounter + i)->ch.toLower())
 					break;
 				++index;
 			}
@@ -366,7 +373,6 @@
 			i = indexOf(ch, i + 1, cs);
 		}
 	}
-
 	return foundIndex;
 }
 
Index: trunk/Scribus/scribus/ui/search.cpp
===================================================================
--- trunk/Scribus/scribus/ui/search.cpp	(revision 22104)
+++ trunk/Scribus/scribus/ui/search.cpp	(working copy)
@@ -37,6 +37,8 @@
 #include "util.h"
 #include "util_text.h"
 
+#include <unicode/utypes.h>
+
 SearchReplace::SearchReplace( QWidget* parent, ScribusDoc *doc, PageItem* ite, bool mode )
 	: QDialog( parent ),
 	matchesFound(0)
@@ -261,7 +263,7 @@
 	if (mode)
 		Word->setEnabled(false);
 	OptsLayout->addWidget( Word );
-	CaseIgnore = new QCheckBox( tr( "&Ignore Case" ), this );
+	CaseIgnore = new QCheckBox( tr( "&Ignore Case/Diacritics/Kashida" ), this );
 	if (mode)
 		CaseIgnore->setEnabled(false);
 	OptsLayout->addWidget( CaseIgnore );
@@ -508,7 +510,25 @@
 			}
 			if (found && searchForReplace)
 			{
-				m_item->itemText.select(a, sText.length());
+				int textLen = 0;
+				if (cs == Qt::CaseSensitive)
+					textLen = sText.length();
+				else
+				{
+					//Case insensitive calculate the length of the word in the text with diacritics
+					for (int j = 0 ; j < sText.length(); j++)
+					{
+						if (u_charType((m_item->itemText.text(a + textLen).unicode())) != UCharCategory::U_NON_SPACING_MARK ||
+								!SpecialChars::isArabicModifierLetter(m_item->itemText.text(a + textLen).unicode()))
+							++textLen;
+
+						while(m_item->itemText.length() > (a + textLen)
+							  && (u_charType((m_item->itemText.text(a + textLen).unicode())) == UCharCategory::U_NON_SPACING_MARK ||
+							  SpecialChars::isArabicModifierLetter(m_item->itemText.text(a + textLen).unicode())))
+							++textLen;
+					}
+				}
+				m_item->itemText.select(a, textLen);
 				m_item->HasSel = true;
 				if (rep)
 				{
@@ -515,7 +535,7 @@
 					DoReplace->setEnabled(true);
 					AllReplace->setEnabled(true);
 				}
-				m_item->itemText.setCursorPosition(a + sText.length());
+				m_item->itemText.setCursorPosition(a + textLen);
 
 				if (!SText->isChecked())
 					break;
@@ -685,36 +705,51 @@
 	{
 		QString repl, sear;
 		int cs, cx;
-// 		ScText *hg;
+		int textLen = 0;
 		if (RText->isChecked())
 		{
 			repl = RTextVal->text();
 			sear = STextVal->text();
-			if (sear.length() == repl.length())
+			if (CaseIgnore->isChecked())
 			{
-				for (cs = 0; cs < sear.length(); ++cs)
+				//case insensitive calculate word length with diacritics
+				for (int j = 0 ; j < sear.length(); j++)
+				{
+					if (!SpecialChars::isArabicModifierLetter(m_item->itemText.text(m_replStart + textLen).unicode()) ||
+							u_charType((m_item->itemText.text(m_replStart + textLen).unicode())) != UCharCategory::U_NON_SPACING_MARK)
+						++textLen;
+					while((unsigned)m_item->itemText.plainText().length() > (m_replStart + textLen) &&
+						  ((SpecialChars::isArabicModifierLetter(m_item->itemText.plainText().at(m_replStart + textLen).unicode())) ||
+						  u_charType((m_item->itemText.text(m_replStart + textLen).unicode())) == UCharCategory::U_NON_SPACING_MARK))
+						++textLen;
+				}
+			}
+			else
+				textLen = sear.length();
+			if (textLen == repl.length())
+			{
+				for (cs = 0; cs < textLen; ++cs)
 					m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
 			}
 			else
 			{
-				if (sear.length() < repl.length())
+				if (textLen < repl.length())
 				{
-					for (cs = 0; cs < sear.length(); ++cs)
+					for (cs = 0; cs < textLen; ++cs)
 						m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
 					for (cx = cs; cx < repl.length(); ++cx)
 						m_item->itemText.insertChars(m_replStart+cx, repl.mid(cx,1), true);
-					// FIXME:NLS also replace styles!!
 				}
 				else
 				{
 					for (cs = 0; cs < repl.length(); ++cs)
 						m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
-					m_item->itemText.removeChars(m_replStart+cs, sear.length() - cs);
+					m_item->itemText.removeChars(m_replStart+cs, textLen - cs);
 				}
-				m_item->itemText.deselectAll();
-				if (repl.length() > 0)
-					m_item->itemText.select(m_replStart, repl.length());
 			}
+			m_item->itemText.deselectAll();
+			if (repl.length() > 0)
+				m_item->itemText.select(m_replStart, repl.length());
 		}
 		if (RStyle->isChecked())
 		{
fixArabic-2.patch (7,115 bytes)

Fahad

2017-07-14 07:36

developer   ~0044175

to test the patch:

In Arabic enter this text بِسْمِ then in search dialog enter بسم and check Ignore Case/Diacritics/Kashida option, if text selected then everything is OK.

In Hebrew enter this text: שְׁוָא and search for this text שוא

In Thai (I am not sure here) enter: ไม้ไต่คู้ and search for ไมไตค source https://en.wikipedia.org/wiki/Thai_alphabet#Diacritics

jghali

2017-07-28 17:04

administrator   ~0044240

Sorry guys for the delay. I did some tests and fixed some issues. So here is a new patch to verify all is still working for you.
Here is a summary of what I fixed:
- Ignoring diacritics was not working when using search&replace dialog from story editor, it now should
- searching "whole words" was not working well, should now work better. Eg: previously searching "test" as whole word would not work in a sequence such as ", test," because of the punctuation
- searching بِسْمِ in "بِسْمِ" with Ignore Case/Diacritics enabled was not finding anything, I tried to provide a fix for this issue

14566_arabicfix_v3.patch (12,189 bytes)
Index: scribus/text/specialchars.cpp
===================================================================
--- scribus/text/specialchars.cpp	(revision 22125)
+++ scribus/text/specialchars.cpp	(working copy)
@@ -263,17 +263,13 @@
 		return false;
 }
 
-bool SpecialChars::isArabicDiacritic(uint ch)
+bool SpecialChars::isArabicModifierLetter(uint ch)
 {
-	if (ch >= 0x064B && ch <= 0x065F) /*Fathatan, Dammatan, Kasratan, Fatha,Damma,Kasra,Shadda,Sukun,Maddah, Hamza Above, Hamza Below,Subscript Alef,Inverted Damma,Mark Noon Ghunna*/
+	if (ch == 0x0640) //ARABIC TATWEEL
 		return true;
-	else if (ch == 0x0640) //Tatweel
+	else if (ch == 0x06E5) //ARABIC SMALL WAW
 		return true;
-	else if (ch >= 0x06D2 && ch <= 0x06DC) /*Yeh Barree,Yeh Barree With Hamza Above, Full Stop Ae,Sad With Lam With Alef,Qaf With Lam With Alef,Small High Meem Initial Form, Small High Lam Alef, Small High Jeem , Small High Three Dots, Small High Seen */
+	else if (ch == 0x06E6) //ARABIC SMALL YEH
 		return true;
-	else if (ch >= 0x06DF && ch <= 0x06E8) /*Small High Rounded Zero, Upright Rectangular Zero,Dotless Head Of Khah,Meem Isolated Form, Small Low Seen, Small High Madda,Small Waw, Small Yeh, Small High Yeh, Small High Noon */
-		return true;
-	else if (ch >= 0x0618 && ch <= 0x061A) /*Small Fatha, Small Damma, Small Kasra*/
-		return true;
 	return false;
 }
Index: scribus/text/specialchars.h
===================================================================
--- scribus/text/specialchars.h	(revision 22125)
+++ scribus/text/specialchars.h	(working copy)
@@ -81,7 +81,7 @@
 	static bool isCJK(uint ch);
 	static bool isLetterRequiringSpaceAroundCJK(uint ch);
 	static bool isIgnorableCodePoint(uint ch);
-	static bool isArabicDiacritic(uint ch);
+	static bool isArabicModifierLetter(uint ch);
 };
 
 #endif
Index: scribus/text/storytext.cpp
===================================================================
--- scribus/text/storytext.cpp	(revision 22125)
+++ scribus/text/storytext.cpp	(working copy)
@@ -311,10 +311,12 @@
 	invalidateAll();
 }
 
-int StoryText::indexOf(const QString &str, int from, Qt::CaseSensitivity cs) const
+int StoryText::find(const QString &str, int from, Qt::CaseSensitivity cs)
 {
 	int foundIndex = -1;
 
+	deselectAll();
+
 	if (str.isEmpty() || (from < 0))
 		return -1;
 
@@ -345,28 +347,123 @@
 			}
 			i = indexOf(ch, i + 1, cs);
 		}
+
+		if (foundIndex >= 0)
+			select(foundIndex, strLen);
+		return foundIndex;
 	}
-	else
+
+	// Case insensitive search
+	int index = 0;
+	int diacriticsCounter = 0; //counter for diacritics
+
+	int i = indexOf(ch, from, cs);
+	while (i >= 0 && i < (int) d->len)
 	{
+		index = diacriticsCounter = 0;
+		while ((index < strLen) && ((index + i + diacriticsCounter ) < storyLen))
+		{
+			const QChar &curChar = d->at(index + diacriticsCounter + i)->ch;
+			if (SpecialChars::isArabicModifierLetter(curChar.unicode()) ||
+				(curChar.category() == QChar::Mark_NonSpacing))
+			{
+				++diacriticsCounter;
+				continue;
+			}
+			if (qStr.at(index) != curChar.toLower())
+				break;
+			++index;
+		}
+		if (index == strLen)
+		{
+			foundIndex = i;
+			break;
+		}
+		i = indexOf(ch, i + 1, cs);
+	}
+
+	if (foundIndex >= 0)
+		select(foundIndex, strLen + diacriticsCounter);
+	return foundIndex;
+}
+
+int StoryText::indexOf(const QString &str, int from, Qt::CaseSensitivity cs, int* pLen) const
+{
+	int foundIndex = -1;
+	if (pLen)
+		*pLen = 0;
+
+	if (str.isEmpty() || (from < 0))
+		return -1;
+
+	QString qStr = str;
+	if (cs == Qt::CaseInsensitive)
+		qStr = qStr.toLower();
+	QChar ch = qStr.at(0);
+
+	int strLen   = qStr.length();
+	int storyLen = length();
+	if (cs == Qt::CaseSensitive)
+	{
 		int i = indexOf(ch, from, cs);
 		while (i >= 0 && i < (int) d->len)
 		{
 			int index = 0;
 			while ((index < strLen) && ((index + i) < storyLen))
 			{
-				if (qStr.at(index) != d->at(index + i)->ch.toLower())
+				if (qStr.at(index) != d->at(index + i)->ch)
 					break;
 				++index;
 			}
 			if (index == strLen)
 			{
 				foundIndex = i;
+				if (pLen)
+					*pLen = strLen;
 				break;
 			}
 			i = indexOf(ch, i + 1, cs);
 		}
 	}
-
+	else
+	{
+		bool qCharIsDiacritic, curCharIsDiacritic;
+		int i = indexOf(ch, from, cs);
+		while (i >= 0 && i < (int) d->len)
+		{
+			int index = 0;
+			int diacriticsCounter = 0; //counter for diacritics
+			while ((index < strLen) && ((index + i + diacriticsCounter) < storyLen))
+			{
+				const QChar &qChar = qStr.at(index);
+				const QChar &curChar = d->at(index + diacriticsCounter + i)->ch;
+				qCharIsDiacritic   = SpecialChars::isArabicModifierLetter(qChar.unicode()) | (qChar.category() == QChar::Mark_NonSpacing);
+				curCharIsDiacritic = SpecialChars::isArabicModifierLetter(curChar.unicode()) | (curChar.category() == QChar::Mark_NonSpacing);
+				if (qCharIsDiacritic || curCharIsDiacritic)
+				{
+					if (qCharIsDiacritic)
+					{
+						++index;
+						--diacriticsCounter;
+					}
+					if (curCharIsDiacritic)
+						++diacriticsCounter;
+					continue;
+				}
+				if (qChar != curChar.toLower())
+					break;
+				++index;
+			}
+			if (index == strLen)
+			{
+				foundIndex = i;
+				if (pLen)
+					*pLen = strLen + diacriticsCounter;
+				break;
+			}
+			i = indexOf(ch, i + 1, cs);
+		}
+	}
 	return foundIndex;
 }
 
Index: scribus/text/storytext.h
===================================================================
--- scribus/text/storytext.h	(revision 22125)
+++ scribus/text/storytext.h	(working copy)
@@ -104,8 +104,11 @@
  	void clear();
 	StoryText copy() const;
 
+	// Find and select text in story
+	int find(const QString &str, int from = 0, Qt::CaseSensitivity cs = Qt::CaseSensitive);
+
 	// Find text in story
-	int indexOf(const QString &str, int from = 0, Qt::CaseSensitivity cs = Qt::CaseSensitive) const;
+	int indexOf(const QString &str, int from = 0, Qt::CaseSensitivity cs = Qt::CaseSensitive, int* pLen = 0) const;
 	int indexOf(QChar ch, int from = 0, Qt::CaseSensitivity cs = Qt::CaseSensitive) const;
 	
 	// Add, change, replace
Index: scribus/ui/search.cpp
===================================================================
--- scribus/ui/search.cpp	(revision 22125)
+++ scribus/ui/search.cpp	(working copy)
@@ -261,7 +261,7 @@
 	if (mode)
 		Word->setEnabled(false);
 	OptsLayout->addWidget( Word );
-	CaseIgnore = new QCheckBox( tr( "&Ignore Case" ), this );
+	CaseIgnore = new QCheckBox( tr( "&Ignore Case, Diacritics and Kashida" ), this );
 	if (mode)
 		CaseIgnore->setEnabled(false);
 	OptsLayout->addWidget( CaseIgnore );
@@ -434,7 +434,7 @@
 
 	uint as = m_item->itemText.cursorPosition();
 	m_replStart = as;
-	int a;
+	int a, textLen(0);
 	if (m_itemMode)
 	{
 		Qt::CaseSensitivity cs = Qt::CaseSensitive;
@@ -446,16 +446,16 @@
 			found = true;
 			if (SText->isChecked())
 			{
-				a = m_item->itemText.indexOf(sText, a, cs);
+				a = m_item->itemText.indexOf(sText, a, cs, &textLen);
 				found = (a >= 0);
 				if (!found) break;
 
-				if (Word->isChecked() && (a > 0) && !m_item->itemText.text(a - 1).isSpace())
+				if (Word->isChecked() && (a > 0) && m_item->itemText.text(a - 1).isLetterOrNumber())
 					found = false;
 				if (Word->isChecked())
 				{
-					int lastChar = qMin(a + sText.length(), maxChar);
-					found = ((lastChar == maxChar) || m_item->itemText.text(lastChar).isSpace());
+					int lastChar = qMin(a + textLen, maxChar);
+					found = ((lastChar == maxChar) || !m_item->itemText.text(lastChar).isLetterOrNumber());
 				}
 				if (!found) continue;
 			}
@@ -508,14 +508,14 @@
 			}
 			if (found && searchForReplace)
 			{
-				m_item->itemText.select(a, sText.length());
+				m_item->itemText.select(a, textLen);
 				m_item->HasSel = true;
 				if (rep)
 				{
 					DoReplace->setEnabled(true);
 					AllReplace->setEnabled(true);
 				}
-				m_item->itemText.setCursorPosition(a + sText.length());
+				m_item->itemText.setCursorPosition(a + textLen);
 
 				if (!SText->isChecked())
 					break;
@@ -551,22 +551,35 @@
 		if (storyTextEdit->StyledText.length() == 0)
 			return;
 
+		QTextCursor cursor = storyTextEdit->textCursor();
+		int position  = cursor.position();
+		StoryText& styledText = storyTextEdit->StyledText;
+		int firstChar = -1, lastChar = styledText.length();
 		if (SText->isChecked())
 		{
-			QTextDocument::FindFlags flags;
-			if (!CaseIgnore->isChecked())
-				flags |= QTextDocument::FindCaseSensitively;
-			if (Word->isChecked())
-				flags |= QTextDocument::FindWholeWords;
-			do
+			Qt::CaseSensitivity cs = Qt::CaseSensitive;
+			if (CaseIgnore->isChecked())
+				cs = Qt::CaseInsensitive;
+
+			for (int i = position; i < styledText.length(); ++i)
 			{
-				found = storyTextEdit->find(sText, flags);
+				i = styledText.indexOf(sText, i, cs, &textLen);
+				found = (i >= 0);
 				if (!found)
 					break;
-				QTextCursor cursor = storyTextEdit->textCursor();
-				int selStart = cursor.selectionStart();
-				for (int ap = 0; ap < sText.length(); ++ap)
+
+				if (Word->isChecked() && (i > 0) && styledText.text(i - 1).isLetterOrNumber())
+					found = false;
+				if (Word->isChecked())
 				{
+					int lastChar = qMin(i + textLen, maxChar);
+					found = ((lastChar == maxChar) || !styledText.text(lastChar).isLetterOrNumber());
+				}
+				if (!found) continue;
+
+				int selStart = i;
+				for (int ap = 0; ap < textLen; ++ap)
+				{
 					const ParagraphStyle& parStyle = storyTextEdit->StyledText.paragraphStyle(selStart + ap);
 					const CharStyle& charStyle = storyTextEdit->StyledText.charStyle(selStart + ap);
 					if (SSize->isChecked() && (charStyle.fontSize() != sSize))
@@ -588,14 +601,17 @@
 					if (SEffect->isChecked() && ((charStyle.effects() & ScStyle_UserStyles) != sEff))
 						found = false;
 				}
-			} while(!found);
+
+				if (found)
+				{
+					firstChar = i;
+					lastChar = i + textLen;
+					break;
+				}
+			}
 		}
 		else
 		{
-			QTextCursor cursor = storyTextEdit->textCursor();
-			int position  = cursor.position();
-			StoryText& styledText = storyTextEdit->StyledText;
-			int firstChar = -1, lastChar = styledText.length();
 			for (int i = position; i < styledText.length(); ++i)
 			{
 				found = true;
@@ -633,15 +649,14 @@
 					break;
 				}
 			}
-
-			found = (firstChar >= 0);
-			if (found)
-			{
-				cursor.setPosition(firstChar);
-				cursor.setPosition(lastChar, QTextCursor::KeepAnchor);
-				storyTextEdit->setTextCursor(cursor);
-			}
 		}
+		found = (firstChar >= 0);
+		if (found)
+		{
+			cursor.setPosition(firstChar);
+			cursor.setPosition(lastChar, QTextCursor::KeepAnchor);
+			storyTextEdit->setTextCursor(cursor);
+		}
 		if (found && searchForReplace)
 		{
 			// m_doc->scMW()->CurrStED->updateProps(); FIXME
@@ -685,36 +700,39 @@
 	{
 		QString repl, sear;
 		int cs, cx;
-// 		ScText *hg;
+		int textLen = 0;
 		if (RText->isChecked())
 		{
 			repl = RTextVal->text();
 			sear = STextVal->text();
-			if (sear.length() == repl.length())
+			textLen = m_item->itemText.lengthOfSelection();
+			if (textLen == repl.length())
 			{
-				for (cs = 0; cs < sear.length(); ++cs)
+				for (cs = 0; cs < textLen; ++cs)
 					m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
 			}
 			else
 			{
-				if (sear.length() < repl.length())
+				if (textLen < repl.length())
 				{
-					for (cs = 0; cs < sear.length(); ++cs)
+					for (cs = 0; cs < textLen; ++cs)
 						m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
 					for (cx = cs; cx < repl.length(); ++cx)
 						m_item->itemText.insertChars(m_replStart+cx, repl.mid(cx,1), true);
-					// FIXME:NLS also replace styles!!
 				}
 				else
 				{
 					for (cs = 0; cs < repl.length(); ++cs)
 						m_item->itemText.replaceChar(m_replStart+cs, repl[cs]);
-					m_item->itemText.removeChars(m_replStart+cs, sear.length() - cs);
+					m_item->itemText.removeChars(m_replStart+cs, textLen - cs);
 				}
-				m_item->itemText.deselectAll();
-				if (repl.length() > 0)
-					m_item->itemText.select(m_replStart, repl.length());
 			}
+			m_item->itemText.deselectAll();
+			if (repl.length() > 0)
+			{
+				m_item->itemText.select(m_replStart, repl.length());
+				m_item->itemText.setCursorPosition(m_replStart + repl.length());
+			}
 		}
 		if (RStyle->isChecked())
 		{
14566_arabicfix_v3.patch (12,189 bytes)

jghali

2017-08-05 02:38

administrator   ~0044258

Could not wait, so I committed ;)

Fahad

2017-08-05 08:13

developer   ~0044259

Hi Jghali, we are sorry to delay to reply back. Actually, Majda finished her internship under my supervision last Thursday, so we were in harry to finish the code and fix any bugs shows up.
And since this patch take a long time and I want to benefit from time we have ( 2 moths internship) I shifted her effort to fix 10477 which is adding find & replace in whole document. The good news is that almost finished the task.
For this patch there is some bugs still not fixed here but we fixed in our work, which are:
- when you replace diacritic-ed word the last diacritic will not be replaces.
- if there is chain text frame this goes wired.
- I don't trust QT in QChar::Mark_NonSpacing, Qt is always is late to update to latest unicode versions, plus we have changed it to use icu::Transliterator as libreoffice does.

the source code for the work here:
https://github.com/alefpress/alefpress/pull/2

The current status now:
- every thing is working fine, you can search and replace into all text frames.
- the logic behind search for all text frames is explained as follow:
 The search algorithm is to start from selected text frame, if there is no
text frame selected select the first text frame in the current page, if
not, find the first text frame from next pages, if reaching the end of
the document start searching from the first page.

There is some enhancements I would like to work on them:
- make search and replace dialog is modeless, so you can search and interact with text directly.
- add the ability to search for ligature (NFC code point) some thing like ff ffi

Issue History

Date Modified Username Field Change
2017-01-18 10:59 Fahad New Issue
2017-07-03 08:00 Majda File Added: fixArabic.patch
2017-07-03 08:00 Majda Note Added: 0044122
2017-07-06 08:48 Fahad Note Added: 0044129
2017-07-06 20:09 cbradney Note Added: 0044131
2017-07-07 04:38 Fahad Note Added: 0044133
2017-07-09 09:30 Majda File Added: arabicfix.patch
2017-07-09 09:31 Majda File Deleted: arabicfix.patch
2017-07-09 09:31 Majda File Added: arabicfix.patch
2017-07-09 09:31 Majda Note Added: 0044152
2017-07-09 13:18 jghali File Added: 14566_ignore_diacritics.patch
2017-07-09 13:18 jghali Note Added: 0044153
2017-07-09 14:10 jghali Note Added: 0044154
2017-07-09 16:11 Fahad Note Added: 0044155
2017-07-10 19:53 jghali Note Added: 0044158
2017-07-11 07:54 Majda Note Added: 0044163
2017-07-11 07:55 Majda File Added: fixArabic-2.patch
2017-07-14 07:36 Fahad Note Added: 0044175
2017-07-28 17:04 jghali File Added: 14566_arabicfix_v3.patch
2017-07-28 17:04 jghali Note Added: 0044240
2017-08-05 02:38 jghali Assigned To => Fahad
2017-08-05 02:38 jghali Status new => resolved
2017-08-05 02:38 jghali Resolution open => fixed
2017-08-05 02:38 jghali Fixed in Version => 1.5.4.svn
2017-08-05 02:38 jghali Note Added: 0044258
2017-08-05 08:13 Fahad Note Added: 0044259
2017-12-20 20:58 cbradney Status resolved => closed