美文网首页
Markup 和 CMarkup 对象

Markup 和 CMarkup 对象

作者: o_0xF2B8F2B8 | 来源:发表于2017-04-07 18:51 被阅读0次

    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() 的合并。

    同样的 previousSiblingfirstChildlastChild 的内部实现流程也类似,通过 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 的子节点。

    相关文章

      网友评论

          本文标题:Markup 和 CMarkup 对象

          本文链接:https://www.haomeiwen.com/subject/miebbttx.html