在 Rashumon 文字处理器开发中开发多语言支持






4.96/5 (38投票s)
在我开发Rashumon时,它没有内置多语言/双向文本支持,我不得不从头开始开发。
引言
本文提供了一些关于 Rashumon 开发的信息,以及由于缺乏SDK来提供基本构建块而不得不从头开发它们的方法。
背景
在1989年至1994年间,我开发了 Rashumon,这是Amiga上的第一款多语言图形文字处理器。Rashumon带来了一些独特的功能:
-
多文本选择(同时选择不连续的文本部分)
-
表格生成器
-
多键盘映射支持(最多同时支持5个)
-
搜索和替换包含颜色、样式和字体过滤器
-
用于创建和重命名文件、文件夹等的**多语言字符串小部件**
-
导入和导出多语言ASCII文件,与PC和MAC之间进行转换
-
超快的屏幕更新和滚动
-
IFF图形支持(导入和导出)。
-
直接访问字体中的每一个256个字符。
Using the Code
本文中使用的代码示例来自Rashumon的源代码,尽管它们是为Amiga Aztec C编译器创建的,但任何C++编译器都可以检查它们。
关注点
如今,我们倾向于忘记20年前编程的复杂性,而这些复杂性现在已经作为任何操作系统SDK的一部分内置了,包括:双向编辑、文本编辑(总体而言)、文本滚动和文本换行。
在1989年为Amiga开发一款多语言图形文字处理器,实际上需要编写一些今天属于操作系统一部分的内容,但当时却缺失了。
开发Rashumon
Amiga曾经并且仍然是一台很棒的计算机,具有强大的功能,尤其是在声音和视频方面。然而,像文件浏览对话框这样的基本元素却缺失了,更不用说对从右到左语言的支持了。
如今,所有操作系统都包含支持多语言文本编辑所需的核心元素。文本是按键入顺序存储的,对于从右到左的语言,会反向显示。这使得编辑和操作文本变得简单方便,因为存储反映了文本流的逻辑顺序。当时,需要开发这样的构建块,如果我以不同于存储的方式显示文本,我的文字处理器会变得太慢。相反,我想开发自己的文本换行引擎。文本换行是允许在不中断单词的情况下换行的机制。与老式打字机不同,打字机在行尾可能会中断单词,文字处理器能够将最后一个输入的单词移到下一行,以防在当前行没有足够空间。
当处理比例字体(每个字符宽度不同)以及允许组合多种字体时,这会变得更加复杂。以上所有内容都不是高层API的一部分,而是需要计算给定文本的预测长度(以像素为单位),并考虑到每个字符的宽度(基于字符和字体)、属性(粗体、斜体)和使用的字号,以及选定的页边距。
因此,即使我们只处理单向(从左到右)编辑,从头开发仍然很复杂。
首先,我编写了计算给定行长度的例程。
int LLen=(mode==GRAPHICS)?ln->Len:(ln->CharLen=strlen(ln->Txt))*8;
正如您所见,有一个简单的情况,其中“mode
”不是“GRAPHICS
”,然后长度是根据字符数乘以8
计算的(当使用等宽字体时,每个字符的长度为8
)。
在使用比例和多种字体编辑双向文本时,即使插入单个字符也更复杂。
// This is a routine for adding a single character, taken
from Rashumon source code
static put_char(wrap,chr)
BOOL wrap;
UBYTE chr;
{
UBYTE c[2];
BOOL update=FALSE;
c[1]='\0';
c[0]=chr;
if(ENGLISH_H)
// Left to right text
{
if(chr>='a' && chr<='z' && map[Header->format >> 11]<2) chr=ucase(chr);
if(Header->Insert || !HCL->Txt[CR])
{
if(!wrap && HCL->Len+font_width(HCL,CR)>HCL->MaxLen) return();
char_insert(HCL,c[0],CR);
HCL->CharLen++;
CR++;
HCL->Len+=font_width(HCL,CR-1);
// Here, we add the additional size to the overall line
//size in pixels
HCL->CursLen+=font_width(HCL,CR-1);
}
/* OVERWRITE IN ENGLISH */
{
// Now, we treat Overwrite mode
HCL->Txt[CR]=c[0];
HCL->Format[CR]=Header->format;
if(c[0]==9)
{
SetFlag(HCL->Format[CR],TAB);
HCL->Txt[CR]=SPACE_CHR;
SetFlag(MD,TABS);
}
CR++;
calc_charlen(HCL);
calc_all(HCL);
Clear(HCL);
}
}
else
// Hebrew ( or Right to Left) mode
{
if(!Header->Insert && CR)
{
CR--;
HCL->CursLen-=font_width(HCL,CR+1);
HCL->Txt[CR]=c[0];
HCL->Format[CR]=Header->format;
if(c[0]==9)
{
SetFlag(HCL->Format[CR],TAB);
HCL->Txt[CR]=SPACE_CHR;
SetFlag(MD,TABS);
}
calc_all(HCL);
Clear(HCL);
}
else
{
if(!wrap && HCL->Len+font_width(HCL,CR)>HCL->MaxLen) return();
char_insert(HCL,c[0],CR);
HCL->Len+=font_width(HCL,CR);
HCL->CharLen++;
}
}
if(HCL->Mode & TABS) calc_all(HCL);
if(c[0]!=SPACE_CHR && fonts[Header->format >> 11]->tf_YSize>LH(HCL))
{
HCL->LineHeight=Header->LineHeight=fonts[Header->format >> 11]->tf_YSize;
HCL->BaseLine=Header->BaseLine=fonts[Header->format >> 11]->
tf_YSize-fonts[Header->format >> 11]->tf_Baseline;
update=TRUE;
}
else
if(c[0]==SPACE_CHR && HCL->prev && !(HCL->Mode & PAR) && wrap)
{
WrapLine(HCL->prev,!(update));
}
if(HCL->Len<=HCL->MaxLen && !(update))
{
showtext(HCL);
SetCursor();
}
else
if(wrap)
FixLine(HCL,!update);
if(update)
update_lh(HCL,TRUE);
}
下一步是对双向行执行文本换行。正如我所解释的,行是按照它们在内存中的存储方式显示的。文本“abcאבג”的存储方式与显示方式完全相同。Rashumon使用了双字节字符,这意味着每个字符都使用2个字节存储。这发生在Unicode发明之前,所以第一个字节足以存储所支持的任何语言中的任何字符。当时,ASCII字符有两种形式:一种形式使用0到127的值,扩展形式使用0到255的值。我使用了扩展形式,并且必须决定将从右到左的语言放在哪里。
从右到左的语言没有标准。IBM使用了128到154的位置,但我发现这存在问题,并选择了从224开始的位置,这似乎是今天的正确选择,因为它与今天使用双字节编码表示从右到左的语言的方式相同。所以,如果我打开1989年的软盘镜像(.ADF文件),所有Rashumon的希伯来语文档都显示正确的编码。
第二个字节用于存储字符颜色(3种类型,意味着最多8种颜色)、字体属性(**粗体**、*斜体*和下划线,或这三者的任意组合)、语言(从右到左或从左到右)以及字体,通过指向在文档使用的整个字体列表中创建的本地列表中的该字体的索引。
/* Line structure */
#define COLOR_BIT_1 1 /* 1 */
#define COLOR_BIT_2 2 /* 2 */
#define COLOR_BIT_3 4 /* 3 */
#define UNDL 8 /* 4 */
#define BOLD 16 /* 5 */
#define ITAL 32 /* 6 */
#define SELECT 64 /* 7 */
#define LANG 128 /* 8 */
#define TAB 256 /* 9 */
#define UNUSED_1 1024 /* 10 */
#define UNUSED_2 2048 /* 11 */
#define FONT_BIT_1 4096 /* 12 */
#define FONT_BIT_2 8192 /* 13 */
#define FONT_BIT_3 16384 /* 14 */
#define FONT_BIT_4 32768 /* 15 */
#define FONT_BIT_5 65536 /* 16 */
键盘映射和编码
键盘映射用作从“1
”开始到数组末尾的每个字符位置的数组。
这是Rashumon源代码的另一部分,其中定义了键盘映射。
/* HEBREW AND ENGLISH MAPS */
unsigned char regmap[] =
";1234567890-=\\0/'-˜€ˆ...\"[] 123(tm)ƒ‚‹'‰‡", 456 †'„-•. .789 ";
unsigned char engmap[] =
"`1234567890-=\\0qwertyuiop[] 123asdfghjkl;' 456zxcvbnm,./ .789 ";
unsigned char shiftmap[] =
"~!@#$%^&*()_+|0QWERTYUIOP{} 123ASDFGHJKL:\" 456 ZXCVBNM<>?.789 ";
unsigned char shiftrus[] =
"~!@#$%^&*()_+|0°¶₪±³¸´¨(r)¯{} 123 ²£¥¦§(c)׫:\" 456 ¹·¢µ¡¬<>? .789 ";
unsigned char rusmap[] =
"`1234567890-=\\0׀ײִׁ׃״װָ־ֿ[] 123ְֳֵֶַֹֺֻׂ;' 456 ׳ֲױֱּֽ,./ .789 ";
正如您所见,“regmap
”是希伯来语编码,“engmap
”用于拉丁文本,“shiftmap
”用于按SHIFT键输入的字符,还有一个俄语键盘映射(后来,也因为John Hajjer,来自芝加哥,他花了大量时间帮助我发布阿拉伯语版本,所以也有一个阿拉伯语的)。
通过一个独特的标尺(有两个版本:从左到右和从右到左)来切换两个方向。
(感谢Shimon Wiessman的图形设计。)
按下箭头,改变了编辑方向。
滚动文本
即使是像滚动这样明显的事情,当时也必须从头发明。这包括根据当前窗口大小(Amiga窗口能够由最终用户调整大小,还可以最大化和最小化)、显示滚动条以及计算滚动条滑块大小(应与可能移动的距离和可用滚动量成比例)来确定要显示的文本行数。
scroll(ln,lines)
struct Line *ln;
int
lines;
{
register SHORT distance,
top=TOP,
bot=BOT;
#if
DEBUG
printf("BEFORE: top=%ld (%ld <> TOP=%ld) ",
Header->top->num,
Header->top->y,TOP+Header->shift);
printf("bottom=%ld (%ld <> BOT=%ld)\n",
Header->bottom->num,
Header->bottom->y+LH(Header->bottom),BOT+Header->shift);
#endif
if(lines>0)
{
distance=Header->bottom->next->y+LH(Header->bottom->next)-Header->
shift-Header->Height;
Header->shift+=distance;
while(Header->top->y<Header->shift)
Header->top=Header->top->next;
Header->bottom=Header->bottom->next;
}
else
{
distance=-(Header->shift-Header->top->prev->y);
Header->shift+=distance;
Header->top=Header->top->prev;
while(Header->bottom->y+LH(Header->bottom)>Header->Height+Header->shift)
Header->bottom=Header->bottom->prev;
}
if(distance<100)
ScrollRaster(rp,0,distance,0,TOP,640,BOT);
else
calc_top_bottom(TRUE,0,0);
#if
DEBUG
printf("AFTER: top=%ld (%ld <> TOP=%ld) ",
Header->top->num,
Header->top->y,TOP+Header->shift);
printf("bottom=%ld (%ld <> BOT=%ld)\n",
Header->bottom->num,
Header->bottom->y+LH(Header->bottom),BOT+Header->shift);
#endif
}
双向文本的文本换行
但现在,让我们回到双向文本的文本换行。基本上,我与Tamer Even – Zohar和她的丈夫Nimrod一起开发的算法是基于检查给定行,如果它比两个页边距之间的距离长(考虑到每个字符的独立属性,以像素为单位计算行宽),我们就应该从行中移除最后一个单词,然后再次检查新长度,以此类推,直到该行在页边距宽度之内。
首先要问的问题是,“最后一个”单词在哪里?如果是一个从右到左的段落,最后一个单词将首先出现在缓冲区中。
在这种情况下,我使用了以下函数,它实际上测量了给定缓冲区中第一个单词的大小(以像素为单位)。以下例程基于等宽字体,即使这样也足够复杂了……
/* returns the len of the first word in s */
#define BLNK(c) ((c)==' '
|| (c)=='\n')
first_wordlen(s,margin,blnks1,blnks2)
char
*s;
int
margin, *blnks1, *blnks2;
{
register
int i, j;
for (i=margin; BLNK(s[i]) && s[i]; i++);
*blnks1 = i;
for (; !(BLNK(s[i])) && s[i]; i++);
for (j=i; BLNK(s[j]) && s[i]; j++);
*blnks2 = j-i;
return(i);
}
如果该行是从左到右的,则使用了不同的函数。
last_wordlen(s,blnks1,blnks2,maxlen)
char
*s;
int
*blnks1, *blnks2, maxlen;
{
register int i, j;
if (!strlen(s)) return(0);
for (i=strlen(s)-1; BLNK(s[i]) && i; i--);
if (i==0) return(0);
*blnks1 = (strlen(s)-(i+1));
for (i=min(maxlen,strlen(s)-1); BLNK(s[i]) && i; i--);
for (; !(BLNK(s[i])) && i; i--);
for (j=i; BLNK(s[j]) && j; j--);
i++;
*blnks2 = i - j;
return(strlen(s)- i);
}
当然,我们不仅从一行中移除最后一个单词,当有可用空间时(例如,如果当前行的第一个单词被删除,并且可用空间增加),我们还会将下一行的第一个单词放回(例如,如果当前行的第一个单词被删除,并且可用空间增加),所以另一个构建块是将下一行的第一个单词放回当前行的末尾。
/* copies first word of length len & trailing blanks
blnks from s2 to s1 */
copy_first(s1,s2,len,blnks)
char
*s1,*s2;
int
len,blnks;
{
append(s2,s1,strlen(s1)+len+(blnks? 1 : 0));
delete1(s2,0,len+(blnks ?1 : 0));
}
在Rashumon中,文本的段落方向是通过检查每一行中每个字符的编码并确定哪个方向占主导地位来自动计算的。在与Tamar Even Zohar和她的丈夫Nimrod头脑风暴时,我们了解到即使是空格“ ”字符也可以有方向,我们必须决定是否想要希伯来语空格字符以及拉丁语空格字符。嗯,这个需求变成了“必须”,因为它对于换行包含多种语言组合的段落是必需的。
例如
“这是一个包含方向相反的语言的段落的示例。 זוהי דוגמה לפיסקה עם שילוב של שתי שפות עם כיוונים מנוגדים"
以下剪辑演示了Rashumon如何编辑双向文本。
现在,如果您更改页边距,哪个单词会“跳”到下一行,或者“跳”回到当前行?唯一确定这一点的方法是知道每个字符的方向(无论是从右到左还是从左到右),包括制表符、空格、逗号等特殊字符。
Rashumon仍可从Aminet下载,地址为 此链接。
延伸阅读
历史
- 2013年10月9日:初始版本