Windows 为了实现浏览器功能代码的复用,将浏览器内部 DOM 接口\DHTML接口使用 COM 方式实现,这样HTML页面的内容就可以方便的被其他各个模块所调用,如 浏览器的javascript、操作浏览器组件的C++等。其主要的实现均存在于 mshtml.dll 中。
其中 Markup 是一系列接口和对象的集合,主要为用户提供访问和修改HTML页面内容的功能。Markup 在IE 浏览器中被封装成一个类,叫做 CMarkup。
Markup
理解 Markup 首先需要理清如下的几个概念
Tags vs Elements
一个html 标签在浏览器内部的表示形式被称为 Element,理解 tag 和 element 的概念尤其重要。HTML 页面内容包含有 tag,如<B></B>、<A></A>等,在使用浏览器访问HTML 页面时,浏览器的 parser 读到 <B></B>等标签,并且根据 tag 的不同创建不同的对象。这些可以操作的对象称为 Element。Markup 所能够操作的也正是这些对象
举例来说,有如下页面
<P>First<P>Second
浏览器页面 parser 解析这些语句时则会变成下面的样子
<HTML><HEAD><TITLE></TITLE></HEAD><BODY>
<P>First</P><P>Second</P></BODY></HTML>
换而言之,paser 将 HTML 页面中的内容转变成了 element,并且添加了一些元素以保证页面结构的完整性。
另一个需要理解的概念是 stream 和 tree。
以如下页面举例
My <B>dog</B> has fleas.
上述页面会被解析成为如下的树结构
ROOT
|
+-----+------+
| | |
"My" B "has fleas."
|
"dog"
而对于上述文档的操作看起来就像是在对树进行操作,比如添加或者删除叶节点。
然而随着功能的加强,页面的内容从 ie4.0 开始变得不再是上图中那样简单的树结构了。
如下一个例子
Where do <B>you <I>want to</B> go</I> today?
在这个页面中 <B> 标签和 <I> 标签相互嵌套,这样一来页面便无法被简单的表示成为树结构,此时 markup 便应运而生了。
Markup 将页面看作是一个 stream。页面中的内容均由markup pointer 进行索引,对于页面内容的操作也是按照markup pointer 指定的范围进行。以上面的页面为例,操作重叠的tag 时使用两个markup pointer ,一个指向tag 的开头另一个指向tag 的结尾,这种方式当然也可以表示之前的树结构,换句话说 stream 是 tree 的超集
合法的和不合法的页面
一般的浏览器都具有容错性,就像上面举过的例子一样,浏览器的 parser 会在解析过程中为页面添加必要的结构以努力构成一个合法页面。一个合法页面至少要包含一个 html、一个head、一个 TITLE 和一个 body。
markup 为用户提供接口,使用户可以在页面解析完成、或者尚未完成时修改页面内容。
IMarkupServices
MarkupContainer
Container 顾名思义即页面Element 的容器,也是Markup 操作的容器,MarkupContainer 用于把创建的Element 对象和页面中的 text 内容联系起来。在页面解析完成之后,系统会默认创建一个主 Container,其后每一次页面内容的操作都需要指定一个 Container,具体的流操作均在这个 Container 上进行。
举例来说,下面的代码想要向一个页面中插入一个元素
int Insert(
MSHTML::IHTMLDocument2Ptr pDoc2,
....)
{
HRESULT hr = S_OK;
//IHTMLDocument2 * pDoc2;
IMarkupServices * pMS;
IMarkupContainer * pMpContainer;
IMarkupPointer * pPtr1, * pPtr2;
pDoc2->QueryInterface( IID_IMarkupContainer, (void **) & pMpContainer);
pDoc2->QueryInterface( IID_IMarkupServices, (void **) & pMS );
// need two pointers for marking
pMS->CreateMarkupPointer( & pPtr1 );
// beginning and ending position.
pMS->CreateMarkupPointer( & pPtr2 );
//
// Set gravity of this pointer so that when the replacement text
// is inserted it will float to be after it.
//
pPtr1->SetGravity( POINTER_GRAVITY_Right ); // Right gravity set
pPtr2->SetGravity( POINTER_GRAVITY_Left );
pPtr1->MoveToContainer( pMpContainer, TRUE );
pPtr2->MoveToContainer( pMpContainer, TRUE );
......
Insert()
}
对页面的插入操作首先需要通过页面对象获取对应的 Container 接口,接着使用 markup pointer 遍历到 Container 中的指定位置,这样才能执行操作。
MarkupPointer
MarkupPointer 并不是 MarkupContainer 的一部分。MarkupPointer 的主要功能是用来指示tag节点在文档中的位置。因此 pointer 可以看作是用于在 Container 中进行索引的迭代器。
举例来说
My <B>d[p1]og</B> has fleas.
在这个页面中,MarkupPointer 出现在[p1]所示的位置上,但它并不会在页面内容中添加任何东西,或者对页面内容进行任何修改。
MarkupPointer 可以被至于页面的这些位置:element的开始、element的结束、或者text之中。由于MarkupPointer本身不包含内容,因此如果两个 MarkupPointer 指向了同一个位置便会难以区分。
通过 Markup ,用户便可以操作页面中的内容,其主要提供了以下一些功能
放置Markup Pointers
markup pointer 被创建后处于 unpositioned 状态,表示它还没有被放置到页面中的任何位置。微软提供了三个函数用来为markup pointer 指定位置
MoveAdjacentToElement
MoveToContainer
MoveToPointer
MoveAdjacentToElement
函数有两个参数,Element和一个枚举类型常量,他们协同指定markup pointer的位置。函数原型如下
HRESULT MoveAdjacentToElement(
IHTMLElement *elementTarget,
ELEMENT_ADJACENCY
);
enum ELEMENT_ADJACENCY {
ELEMENT_ADJ_BeforeBegin
ELEMENT_ADJ_AfterBegin
ELEMENT_ADJ_BeforeEnd
ELEMENT_ADJ_AfterEnd
};
MoveToContainer
函数也有两个参数,MarkupContainer 和一个Bool 类型用以指定 markup pointer 应该放在 container 的开始还是结尾。函数原型如下
HRESULT MoveToContainer(
IMarkupContainer *containerTarget,
BOOL fAtStart
);
MoveToPointer
函数只有一个参数,另一个markup pointer。函数功能即把当前 pointer 指定到参数 pointer 的位置。函数原型如下
HRESULT MoveToPointer(
IMarkupPointer *pointerTarget
);
这个函数一般用于在markup pointer执行功能的时候,保存当前的位置
比较pointer 的位置
两个 markup pointer 的位置关系可以使用下面的函数进行比较
HRESULT IsEqualTo(
IMarkupPointer *compareTo,
BOOL *fResult
);
HRESULT IsLeftOf(
IMarkupPointer *compareTo,
BOOL *fResult
);
HRESULT IsLeftOfOrEqualTo(
IMarkupPointer *compareTo,
BOOL *fResult
);
HRESULT IsRightOf(
IMarkupPointer *compareTo,
BOOL *fResult
);
HRESULT IsRightOfOrEqualTo(
IMarkupPointer *compareTo,
BOOL *fResult
);
Navigating the Pointer
一旦一个 markup pointer 被放置在一个 markup containter 中。用户便可以使用这个 pointer 来检查周围的页面内容,或者遍历这块内容。用户只能使用windows 提供的两个函数完成这些功能,Left检查pointer 的左边是什么,Right 检查pointer 的右边是什么
HRESULT Left(
BOOL fMove,
MARKUP_CONTEXT_TYPE pContextType,
IHTMLElement **ppElement,
long *plCch,
OLE_CHAR *pch
);
HRESULT Right(
BOOL fMove,
MARKUP_CONTEXT_TYPE pContextType,
IHTMLElement **ppElement,
long *plCch,
OLE_CHAR *pch
);
- 第一个参数指定指针是否可移动,若不可移动,则函数仅仅会返回指针周围内容的描述;否则,函数在返回周围内容描述的同时还会移动过去。
- 第二个参数为返回值,返回pointer周围的内容类型。
Value | Are | Example |
---|---|---|
CONTEXT_TYPE_None | pointer左边或者右边没有东西 | [p1]<HTML></HTML>[p2] |
CONTEXT_TYPE_Text | pointer左边或者右边是一个text | tex[p]t |
CONTEXT_TYPE_EnterScope | 如果是Left,则point左边是一个End tag;如果是Right,pointer的右边是一个Begin tag 。 | </B>[p]<B> |
CONTEXT_TYPE_ExitScope | 如果是Left,则point左边是一个Begin tag;如果是Right,pointer的右边是一个End tag 。 | <B>[p]</B> |
CONTEXT_TYPE_NoScope | pointer的左边或者右边不是一个可以成对的标签 | <BR>[p]<BR> |
- 第三个参数返回 pointer 左边或者右边的element
- 第四个参数用来限定读取的text范围,同时也用来返回获取的text 的大小
- 第五个参数返回pointer 左边或者右边的 text
下面以具体的页面举例说明
[p1]Where [p2]<I>do </I>[p3]<B>you <BR>[p4]want</B> to go today[p5]?
对于页面上的五个pointer 分别调用left,right结果如下表
Ptr | Derection | Type | Element | cch in | cch out | Text |
---|---|---|---|---|---|---|
p1 | left | None | - | - | - | - |
p1 | right | Text | - | 2 | 2 | Wh |
p1 | right | Text | - | -1 | 6 | - |
p1 | right | Text | - | 345 | 6 | Where |
p2 | left | Text | - | NULL | - | - |
p2 | right | EnterScope | I | - | - | - |
p3 | left | ExitScope | I | - | - | - |
p4 | left | NoScope | BR | - | - | - |
p5 | left | Text | I | 100 | 12 | NULL |
CurrentScope
函数可以得到Pointer 当前指向的Element。函数原型如下
HRESULT CurrentScope(
IHTMLElement **ppElementCurrent
);
上述例子中,p1返回值是 NULL;p4返回值是B,因为BR不是一个可以成对的标签
Pointer Gravity
通常情况下,一个 document 被修改之后,document 中的markup Pointer还会保留在之前未修改时的位置。
举例来说
abc[p1]defg[p2]hij
abc[p1]deXYZfg[p2]hij
当第一个页面被修改为第二个页面之后,虽然页面的内容发生了改变,但是pointer 的相对位置仍然保持不变。
但如果页面的修改发生在 point 指向的位置,如上例中,向c、d之间插入一个Z,p 的位置就会出现二义性。
abcZ[p1]de or abc[p1]Zde
这时就需要引用另一个重要的概念gravity,每一个pointer都有一个 gravity 值标识着其左偏或右偏。仍以上述页面为例
abc[p1,right]defg[p2,left]hij
分别在p1,p2的位置插入一对<B>标签。这时由于gravity的存在,页面会变成如下
abc<B>[p1,right]defg[p2,left]</B>hij
默认情况下 pointer 的gravity 值是 left。用户可以通过 windows 提供的函数来查看或者修改 pointer 的 gravity 值
enum POINTER_GRAVITY {
POINTER_GRAVITY_Left,
POINTER_GRAVITY_Right
};
HRESULT Gravity(
POINTER_GRAVITY *pGravityOut
);
HRESULT SetGravity(
POINTER_GRAVITY newGravity
);
Pointer Cling
有如下例子
[p2]ab[p1]cdxy
当bc 段被移动到 xy之间时p1的位置也出现了二义性,是应该随着bc移动,还是应该继续保持在原位呢
[p2]a[p1]dxbcy or [p2]adxb[p1]cy
这就需要 cling 的存在,如果p1指定了cling 属性,那么页面操作之后就会成为右边所示的情况,否则就会出现左边所示的情况
cling 和 gravity 可以协同作用,比如下面的例子
a[p1]bcxy
b移动到x、y之间,如果p1指定了 cling属性,并且gravity 值为 right,那么p1便会跟随b一起到xy之间。这种情况下如果b被删除,那么p1也会跟着从content 中移除,但并不会销毁,因为p1还有可能重新被使用
cling相关的函数,函数原型如下
HRESULT Cling(
BOOL *pClingOut
);
HRESULT SetCling(
BOOL NewCling
);
创建新Element
动态创建新节点的操作也是通过 markup 来完成的,CreateElement 函数原型如下
enum ELEMENT_TAG_ID {
TAGTADID_A,
TAGTADID_ACRONYM,
..
TAGTADID_WBR,
TAGTADID_XMP
};
HRESULT CreateElement(
TAG_ID tagID,
OLECHAR *pchAttrs,
IHTMLElement **ppNewElement
);
第二个参数是属性串,可以在 Element创建时就加入属性。
用户也可以通过从一个已有 element 克隆,来得到新的 element
插入新 Element
新 element 成功创建之后,如果想加入document 中,还需要通过markup 将element插入。 函数原型如下
HRESULT InsertElement(
IHTMLElement *pElementInsertThis,
IMarkupPointer *pPointerStart,
IMarkupPointer *pPointerFinish
);
第二参数指示这个element 的begin tag 插入到哪里;第三个参数指示这个 element 的end tag应该插入到哪里;这两个位置必须在同一个 markup Container 中。
举例来说,调用函数将 <B> 标签插入下面的页面中
My [pstart]dog[pend] has fleas.
默认情况下结果将如下面所示,如果 pointer 的 gravity 改变,情况也会改变
My [pstart]<B>dog[pend]</B> has fleas.
移除Element
移除 element 并不需要markup pointer ,只需要传递给函数要删除的 element 就可以。函数原型如下
HRESULT RemoveElement(
IHTMLElement *pElementRemoveThis
);
element 被从 document 中移除之后并不会被删除,他随时可以被重新插入
插入 Text
在 document 中插入 text ,函数原型如下
HRESULT InsertText(
OLECHAR *pch,
long cch,
IMarkupPointer *pPointerTarget
);
注意到,插入text 只需要一个 markup pointer 来指定位置
移除内容
用户可以移除在同一个container 中一段连续的内容,函数原型如下
HRESULT Remove(
IMarkupPointer *pPointerSourceStart,
IMarkupPointer *pPointerSourceFinish
);
两个参数用来指定remove操作的范围,所有在这两个点之间的内容都会被移除。但是有一点例外,即两个 pointer 没有完全包含的 element 不会被移除。举例来说
<--------- i -----------> <---------- u ----------->
a<I>b<B>c[pstart]d<S>e</I>f<U>g</S>h[pend]hi</B>j</U>kl
<----- s ------->
remove 操作传入 pstart、pend 两个参数,结果页面被修改为下面的情况
<------- i --------><------- u -------->
a<I>b<B>c[pstart]</I><U>[pend]hi</B>j</U>kl
<U> 和 <I> 并未被移除。
替换内容
插入和移除操作何以合成 Replace 操作
int MarkupSvc::RemoveNReplace(
MSHTML::IHTMLDocument2Ptr pDoc2,
_bstr_t bstrinputfrom, _bstr_t bstrinputto)
{
HRESULT hr = S_OK;
//IHTMLDocument2 * pDoc2;
IMarkupServices * pMS;
IMarkupContainer * pMarkup;
IMarkupPointer * pPtr1, * pPtr2;
TCHAR * pstrFrom = _T( bstrinputfrom );
TCHAR * pstrTo = _T( bstrinputto );
pDoc2->QueryInterface( IID_IMarkupContainer, (void **) & pMarkup );
pDoc2->QueryInterface( IID_IMarkupServices, (void **) & pMS );
// need two pointers for marking
pMS->CreateMarkupPointer( & pPtr1 );
// beginning and ending position of text.
pMS->CreateMarkupPointer( & pPtr2 );
//
// Set gravity of this pointer so that when the replacement text
// is inserted it will float to be after it.
//
pPtr1->SetGravity( POINTER_GRAVITY_Right ); // Right gravity set
//
// Start the search at the beginning of the primary container
//
pPtr1->MoveToContainer( pMarkup, TRUE );
for ( ; ; )
{
hr = pPtr1->FindText( (unsigned short *) pstrFrom, 0, pPtr2, NULL );
if (hr == S_FALSE) // did not find the text
break;
// found it, removing..
pMS->Remove( pPtr1, pPtr2 );
//inserting new text
pMS->InsertText( (unsigned short *) pstrTo, -1, pPtr1 );
}
if (hr == S_FALSE) return FALSE;
else return(TRUE);
}
移动内容
用户可以使用 Move 移动一段页面内容,函数原型如下
HRESULT Move(
IMarkupPointer *pPointerSourceStart,
IMarkupPointer *pPointerSourceFinish,
IMarkupPointer *pPointerTarget
);
函数前两个参数和 remove 类似,函数会将这一整段内容移动到目的 pointer 中。那些与pointer 范围有重叠的 element,即并不完全包含在 pointers 之间的 element 会在目的处创建一个拷贝。
举例来说
X[pdest]Y
<--------- i -----------> <---------- u ----------->
a<I>b<B>c[pstart]d<S>e</I>f<U>g</S>h[pend]hi</B>j</U>kl
<----- s ------->
操作之后页面变成
X[pdest]<I'>d<S>e</I'>f<U'>g</S>h</U'>Y
<------- i --------><------- u -------->
a<I>b<B>c[pstart]</I><U>[pend]hi</B>j</U>kl
可以看到完全包含在pointers 中的<S>标签被移动到dest 位置,而与 pointers 区域重叠的 <U>、<I>标签在目标位置创建一个备份。
以上内容翻译自微软提供的官方 Markup Serivce 文档。
CMarkup
CMarkup 其本质上是对Markup Service 的封装,在 IE/EDGE 中方便 js 引擎在操作页面时调用。简单来说 CMarkup 可以看作是 Markup Service 中的 MarkupContainer。以下是 IE8 中 CMarkup 的部分结构,可以看出其关联了与页面相关的许多重要的元素。不仅如此所有的页面元素都保存一个指向 CMarkup 的指针,在对页面元素进行访问时,均需要通过 CMarkup 来进行。
Class CMarkup{
+0xA0 WindowedMarkupContext
+0x40 CDocument
+0x108 COmWindowProxy
+0x50 CHtmlCtx
+0x54 CProgSink
+0x5C CSecurityContext
+0x8c CAPStatr
+0xc0 CSecurityContext
+0xc8 CStyleSheetArray
+0xcc TagArray
+0xd0 ComWindowProxy
+0xdc obj_name_space
+0xf4 CHtmlElemeCtxStream
+0x124 uri
+0x158 CTimeManager
+0x16c CMSPerformanceData
+0x140 CTreePos
}
以 DOM 节点固有属性 nextSibling 举例,该属性用于返回其父节点的 childNodes 列表中紧跟在其后的节点。通过 js 访问节点的该属性,IE 8 内部使用 CElement::get_nextSibling
函数来实现,对该函数进行逆向后部分代码如下。
HRESULT CElement::GetNextSiblingHelper(CElement *this, CElement **nextSibling)
{
CMarkupPointer * markupPointer;
CDoc* cDoc;
HRESULT result;
cDoc = CElement::Doc(this);
CMarkupPointer::CMarkupPointer(markupPointer, cDoc); // 创建 MarkupPointer
result = markupPointer->MoveAdjacentToElement( this, ELEMENT_ADJ_AfterEnd); // 放置 MarkupPointer
if ( result == S_OK )
{
cDoc = CElement::Doc(this);
result = sub_74D4A0B3(cDoc , markupPointer, &nextSibling); // 通过 MarkupPoint 获取 Element
}
result = CBase::SetErrorInfo(markupPointer, result);
CMarkupPointer::~CMarkupPointer(markupPointer);
return result;
}
函数的主要逻辑即,首先新建一个 MarkupPointer 对象,接着将该 MarkupPointer 放置于目标节点的 ELEMENT_ADJ_AfterEnd
位置,而后通过该 MarkupPointer 来检查周围的内容,这里使用的函数其实是 CMarkupPointer::There
,其函数为 Left() 和 Right() 的合并。
同样的 previousSibling 、firstChild 、lastChild 的内部实现流程也类似,通过 CMarkupPointer::MoveAdjacentToElement
将 CMarkupPointer 放置在节点对象的不同位置,再通过 CMarkupPointer::There
取出对应的节点信息即可。
childNodes 节点属性则是通过 CMarkupPointer 遍历对应 Element 节点而实现,在 IE 8 中其主要的功能函数为 CElement::DOMEnumerateChildren
,该函数逆向后主要功能代码如下
CElement::DOMEnumerateChildren(CElement children[])
{
cDoc = CElement::Doc(this);
CMarkupPointer::CMarkupPointer(markupPtrBegin, cDoc);
CMarkupPointer::CMarkupPointer(markupPtrEnd, cDoc);
......
result = markupPtrBegin->MoveAdjacentToElement( this, ELEMENT_ADJ_AfterBegin); // 放置 MarkupPointer
result = markupPtrEnd->MoveAdjacentToElement( this, ELEMENT_ADJ_AfterEnd); // 放置 MarkupPointer
do{
......
child = markupPointer->There()
children[i++] = child;
result = markupPtrBegin->MoveAdjacentToElement( child, ELEMENT_ADJ_AfterBegin); // 放置 MarkupPointer
......
}while( !markupPtrBegin->isLeftOf(markupPtrEnd) )
......
}
通过两个 CMarkupPointer 指针分别指向 Element 的开始和结尾,从 Element 的开始位置依次遍历 ,其间所有的节点均为 Element 的子节点。