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 的子節點。